diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 0293b4bd04..94241e8a75 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -406,7 +406,12 @@ export function ensureStrictCatalogFields( if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = false; if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none"; if (typeof entry.support_verbosity !== "boolean") entry.support_verbosity = true; - if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low"; + // A row that has declared it does NOT support verbosity must not also ship a default for the + // control it just disowned: Codex seeds its picker from `default_verbosity`, so leaving the + // strict-fields fallback in place re-creates the dead toggle the explicit opt-out removed. + // Scoped to an explicit `false`, so rows that never declare a capability keep the default. + if (entry.support_verbosity === false) delete entry.default_verbosity; + else if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low"; if (typeof entry.apply_patch_tool_type !== "string") entry.apply_patch_tool_type = "freeform"; if (!entry.truncation_policy || typeof entry.truncation_policy !== "object" || Array.isArray(entry.truncation_policy)) { entry.truncation_policy = { mode: "tokens", limit: 10000 }; diff --git a/tests/catalog-verbosity-default.test.ts b/tests/catalog-verbosity-default.test.ts new file mode 100644 index 0000000000..75a07cca66 --- /dev/null +++ b/tests/catalog-verbosity-default.test.ts @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { buildCatalogEntries, gatherRoutedModels as gatherRoutedModelsDirect, upstreamNativeEntry } from "../src/codex/catalog"; +import { withStubbedProviderFetch } from "./helpers/catalog-provider-fetch"; +import { resetCatalogRuntimeStateForTests, resetOpenAiApiCatalogWarningStateForTests } from "../src/codex/catalog"; +import { clearModelCache } from "../src/codex/model-cache"; + +const gatherRoutedModels: typeof gatherRoutedModelsDirect = (config, options) => + gatherRoutedModelsDirect(withStubbedProviderFetch(config), options); + +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache(); + resetOpenAiApiCatalogWarningStateForTests(); + resetCatalogRuntimeStateForTests(); +}); + +const originalFetch = globalThis.fetch; + +/** + * A serialized row that declares `support_verbosity: false` must not also carry a + * `default_verbosity`. Codex seeds its picker from `default_verbosity`, so leaving the + * strict-fields fallback in place re-creates the dead toggle that the explicit opt-out + * (#2578 architecture) removed. All field names here are the SERIALIZED Codex spellings — + * `supports_verbosity` does not exist in this format, which is exactly how an earlier + * assertion passed while every routed row advertised the control. + */ +describe("catalog — default_verbosity is dropped when verbosity is unsupported", () => { + test("GREEN: an opted-out routed row carries no verbosity default", async () => { + const models = await gatherRoutedModels({ + providers: { + xai: { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + authMode: "oauth", + liveModels: false, + models: ["grok-4.6"], + }, + }, + }); + const entries = buildCatalogEntries(null, [], models); + const xai = entries.find(e => e.slug === "xai/grok-4.6"); + expect(xai?.support_verbosity).toBe(false); + expect(xai?.default_verbosity).toBeUndefined(); + }); + + test("GREEN: a Kiro opted-out row carries no verbosity default either", async () => { + const models = await gatherRoutedModels({ + providers: { + kiro: { + adapter: "kiro", + baseUrl: "https://runtime.us-east-1.kiro.dev", + authMode: "oauth", + liveModels: false, + models: ["gpt-5.6-sol"], + }, + }, + }); + const entries = buildCatalogEntries(null, [], models); + const kiro = entries.find(e => e.slug === "kiro/gpt-5.6-sol"); + expect(kiro?.support_verbosity).toBe(false); + expect(kiro?.default_verbosity).toBeUndefined(); + }); + + test("CONTROL: rows that never declare a capability keep the permissive default", async () => { + const models = await gatherRoutedModels({ + providers: { + plain: { + adapter: "openai-responses", + baseUrl: "https://plain.example.test/v1", + authMode: "key", + liveModels: false, + models: ["plain-model"], + }, + }, + }); + const entries = buildCatalogEntries(null, [], models); + const plain = entries.find(e => e.slug === "plain/plain-model"); + expect(plain?.support_verbosity).toBe(true); + expect(plain?.default_verbosity).toBe("low"); + }); + + test("CONTROL: a native OpenAI row keeps verbosity and its default", () => { + const template = upstreamNativeEntry("gpt-5.6-sol"); + expect(template).not.toBeNull(); + const entries = buildCatalogEntries(template, ["gpt-5.6-sol"], []); + const native = entries.find(e => e.slug === "gpt-5.6-sol"); + expect(native?.support_verbosity).toBe(true); + expect(native?.default_verbosity).toBe("low"); + }); +});