diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index ef1c68922bb..405bf09de26 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -939,6 +939,7 @@ "management-provider-proto-override.test.ts": "server", "management-provider-synthetic-max.test.ts": "server", "management-provider-validation.test.ts": "server", + "management-provider-verbosity.test.ts": "server", "management-route-registry.test.ts": "server", "management-workflow-budget-routes.test.ts": "server", "memory-watchdog.test.ts": "server", diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index c8f53c861ae..49d6f9e65cd 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -801,6 +801,8 @@ export function providerManagementConfigError( if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`; const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax"); if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`; + const verbositySupportError = booleanRecordConfigError(raw.modelSupportsVerbosity, "modelSupportsVerbosity"); + if (verbositySupportError) return `provider ${name} ${verbositySupportError}`; const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError( raw.modelReasoningSummaryDelivery, raw.modelSupportsReasoningSummaries, diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index d8a39f76b8b..da054536441 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -765,6 +765,7 @@ "management-provider-proto-override.test.ts": "server", "management-provider-synthetic-max.test.ts": "server", "management-provider-validation.test.ts": "server", + "management-provider-verbosity.test.ts": "server", "management-route-registry.test.ts": "server", "management-workflow-budget-routes.test.ts": "server", "memory-watchdog.test.ts": "server", diff --git a/tests/server/management-provider-verbosity.test.ts b/tests/server/management-provider-verbosity.test.ts new file mode 100644 index 00000000000..75f8e65deea --- /dev/null +++ b/tests/server/management-provider-verbosity.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { loadConfig, saveConfig } from "../../src/config"; +import { startServer } from "../../src/server"; +import { installIsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { managementFetch as fetch } from "../helpers/management-auth"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +test("provider management validates verbosity records without replacing valid config", async () => { + const testDir = mkdtempSync(join(tmpdir(), "ocx-provider-verbosity-")); + const previousHome = process.env.OPENCODEX_HOME; + const previousToken = process.env.OPENCODEX_API_AUTH_TOKEN; + const isolatedCodexHome = installIsolatedCodexHome("ocx-provider-verbosity-codex-"); + process.env.OPENCODEX_HOME = testDir; + let server: ReturnType | undefined; + try { + saveConfig({ + port: 0, + defaultProvider: "openai", + openaiProviderTierVersion: 2, + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "direct", + }, + }, + }); + server = startServer(0); + const acceptedVerbosityCapability = await fetch(new URL("/api/providers", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "custom-verbosity-capability", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.example.test/v1", + modelSupportsVerbosity: { terse: false }, + }, + }), + }); + expect(acceptedVerbosityCapability.status).toBe(200); + for (const invalid of [[], { terse: "false" }, { "": false }]) { + const rejected = await fetch(new URL("/api/providers", server.url), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: "custom-verbosity-capability", + provider: { + adapter: "openai-responses", + baseUrl: "https://api.example.test/v1", + modelSupportsVerbosity: invalid, + }, + }), + }); + expect(rejected.status).toBe(400); + } + expect(loadConfig().providers["custom-verbosity-capability"].modelSupportsVerbosity).toEqual({ terse: false }); + } finally { + await server?.stop(true); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + if (previousToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; + else process.env.OPENCODEX_API_AUTH_TOKEN = previousToken; + isolatedCodexHome.restore(); + removeTreeWithRetry(testDir); + } +}, 60_000);