From 867d9562e60165d712ab2ab47486c3261b2fd528 Mon Sep 17 00:00:00 2001 From: Nhat Bui Date: Mon, 17 Aug 2026 09:49:17 +0700 Subject: [PATCH 1/2] feat(catalog): per-model preserveExactReasoningRungs to stop synthetic max/ultra rungs Adds OcxProviderConfig.preserveExactReasoningRungs (provider-wide) and modelPreserveExactReasoningRungs (per-model) so operators can advertise exactly the reasoning ladder a routed model actually supports. When enabled, the Codex catalog omits the synthetic max/ultra rungs that applyReasoningLevels appends, matching the picker to real provider capability (e.g. alibaba-token-plan-intl qwen3.8-max -> [low, medium, xhigh]). Closes #1870 --- .../docs/reference/configuration/providers.md | 2 + src/codex/catalog/parsing.ts | 5 ++ src/codex/catalog/provider-fetch.ts | 3 + src/codex/catalog/sync.ts | 10 ++- src/config.ts | 19 +++++ src/providers/model-rename-migration.ts | 1 + src/types.ts | 13 ++++ tests/preserve-exact-reasoning-rungs.test.ts | 72 +++++++++++++++++++ 8 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 tests/preserve-exact-reasoning-rungs.test.ts diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 0b1b4d7627..0a2cdd223a 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -96,6 +96,8 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `refreshPolicy?` | `"proactive" \| "lazy-only" \| "disabled"` | Override this OAuth provider's Token Guardian policy. | | `reasoningEfforts?` | `string[]` | Provider-wide Codex reasoning labels to advertise and send. For `google`-adapter providers, a configured ladder also asserts `thinkingLevel` capability: direct and Vertex non-image requests send the selected effort as `generationConfig.thinkingConfig.thinkingLevel`, while Cloud Code Assist uses its envelope-specific path. | | `modelReasoningEfforts?` | `Record` | Per-model labels. An empty list hides effort control. As with `reasoningEfforts`, each configured `google`-adapter ladder asserts `thinkingLevel` capability; direct and Vertex non-image requests use the flat Gemini path, while Cloud Code Assist sends it under its request envelope. | +| `preserveExactReasoningRungs?` | `boolean` | Suppress the synthetic `max`/`ultra` rungs that opencodex normally appends to every reasoning-capable routed model. When `true`, the catalog advertises exactly the configured `reasoningEfforts` / `modelReasoningEfforts` ladder so the Codex picker reflects a model's real capability. Removing `max` can hard-fail subagent `spawn_agent` effort overrides, which codex-rs validates against catalog membership — enable only for models that genuinely lack the top rungs. | +| `modelPreserveExactReasoningRungs?` | `Record` | Per-model override for `preserveExactReasoningRungs`. | | `modelSupportsReasoningSummaries?` | `Record` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. | | `modelReasoningSummaryDelivery?` | `Record` | Per-model Responses delivery enum; rewrites an existing delivery field. | | `modelAdapters?` | `Record` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults. The OpenCode Go preset selects Responses for `gpt-5.6-luna` while leaving sibling models on their documented wires; DeepSeek can select native Responses for `deepseek-v4-flash`; and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. | diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index f42049150e..b966ee6697 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -109,6 +109,11 @@ export interface CatalogModel { owned_by?: string; reasoningEfforts?: string[]; defaultReasoningEffort?: string; + /** + * Suppress the synthetic `max`/`ultra` top rungs so the catalog advertises exactly + * `reasoningEfforts` (mirrors `OcxProviderConfig.preserveExactReasoningRungs`). + */ + preserveExactReasoningRungs?: boolean; contextWindow?: number; maxInputTokens?: number; contextCap?: number; diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index c858524eff..c763e67194 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -645,6 +645,8 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, } const reasoningEfforts = configuredReasoningEfforts(prov, model.id); const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort; + const preserveExactReasoningRungs = modelRecordValue(prov.modelPreserveExactReasoningRungs, model.id) + ?? prov.preserveExactReasoningRungs; const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id); const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name); const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model; @@ -667,6 +669,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, } : {}), ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), + ...(preserveExactReasoningRungs === true ? { preserveExactReasoningRungs: true } : {}), ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), ...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}), diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 9ee2886028..50f4e8496e 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -285,6 +285,12 @@ export function deriveEntry( const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true ? upstreamNativeEntry(model.id) : null; + // Exact-combo rows and the ChatGPT forward surface already advertise the real ladder; + // `preserveExactReasoningRungs` extends that to ordinary routed models whose configured + // ladder omits the synthetic `max`/`ultra` top rungs (#1870). + const preserveExactReasoningRungs = preserveExact + || codexForwardNativeCapabilityAlias !== null + || model?.preserveExactReasoningRungs === true; const isRouted = model !== undefined; if (!isRouted && !slug.includes("/")) { // Supported native slug covered by the upstream snapshot: use the REAL entry (exact @@ -326,7 +332,7 @@ export function deriveEntry( e, model?.reasoningEfforts, model?.defaultReasoningEffort, - preserveExact || codexForwardNativeCapabilityAlias !== null, + preserveExactReasoningRungs, ); // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned // native tool/search/responses-lite contract while preserving the routed slug and wire id. @@ -374,7 +380,7 @@ export function deriveEntry( }; if (isRouted) { applyRoutedCodexToolMode(entry); - applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); + applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoningRungs); } else { applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]); diff --git a/src/config.ts b/src/config.ts index d4c0a3a0f3..74bfa5777a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1535,6 +1535,25 @@ const configSchema = z.object({ message: reasoningSummariesError, }); } + const preserveExactReasoningRungsError = booleanRecordConfigError( + (provider as { modelPreserveExactReasoningRungs?: unknown }).modelPreserveExactReasoningRungs, + "modelPreserveExactReasoningRungs", + ); + if (preserveExactReasoningRungsError) { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "modelPreserveExactReasoningRungs"], + message: preserveExactReasoningRungsError, + }); + } + if ((provider as { preserveExactReasoningRungs?: unknown }).preserveExactReasoningRungs !== undefined + && typeof (provider as { preserveExactReasoningRungs?: unknown }).preserveExactReasoningRungs !== "boolean") { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "preserveExactReasoningRungs"], + message: "preserveExactReasoningRungs must be a boolean", + }); + } const serviceTierModelsError = booleanRecordConfigError( (provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier, "modelSupportsServiceTier", diff --git a/src/providers/model-rename-migration.ts b/src/providers/model-rename-migration.ts index 3895386b58..72ab649209 100644 --- a/src/providers/model-rename-migration.ts +++ b/src/providers/model-rename-migration.ts @@ -88,6 +88,7 @@ const MODEL_KEYED_RECORDS = [ "modelReasoningEfforts", "modelDefaultReasoningEfforts", "modelReasoningEffortMap", + "modelPreserveExactReasoningRungs", ] as const; /** Provider fields that are flat lists of model ids. */ diff --git a/src/types.ts b/src/types.ts index 1773a2f83c..bee8dcd1bf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1494,6 +1494,19 @@ export interface OcxProviderConfig { reasoningEfforts?: string[]; /** Model-specific Codex-visible reasoning tiers. An empty array means “do not expose effort”. */ modelReasoningEfforts?: Record; + /** + * Suppress the synthetic `max`/`ultra` reasoning rungs that `applyReasoningLevels` + * appends to every reasoning-capable routed model. When true, the catalog advertises + * exactly the configured `reasoningEfforts` / `modelReasoningEfforts` ladder instead of + * padding the top rungs, so the Codex picker reflects the model's real capability. + * + * Tradeoff: subagent `spawn_agent` sends `max` directly and codex-rs validates it + * against catalog membership, so removing `max` can hard-fail that path for the affected + * models. Enable only for models that cannot actually serve the synthetic top rungs. + */ + preserveExactReasoningRungs?: boolean; + /** Per-model override for `preserveExactReasoningRungs`. */ + modelPreserveExactReasoningRungs?: Record; /** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */ modelDefaultReasoningEfforts?: Record; /** diff --git a/tests/preserve-exact-reasoning-rungs.test.ts b/tests/preserve-exact-reasoning-rungs.test.ts new file mode 100644 index 0000000000..df77e2a8a9 --- /dev/null +++ b/tests/preserve-exact-reasoning-rungs.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from "bun:test"; +import { applyProviderConfigHints, buildCatalogEntries } from "../src/codex/catalog"; +import type { OcxProviderConfig } from "../src/types"; + +function nativeTemplate(): Record { + return { + slug: "gpt-5.5", + display_name: "gpt-5.5", + description: "Native GPT model", + priority: 1, + visibility: "list", + base_instructions: "You are Codex, a coding agent based on GPT-5.", + supported_reasoning_levels: [ + { effort: "low", description: "native low" }, + { effort: "medium", description: "native medium" }, + { effort: "high", description: "native high" }, + { effort: "xhigh", description: "native xhigh" }, + ], + }; +} + +function provider(overrides: Partial = {}): OcxProviderConfig { + return { + adapter: "openai-chat", + baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", + apiKey: "sk-test", + modelReasoningEfforts: { "qwen3.8-max": ["low", "medium", "xhigh"] }, + ...overrides, + }; +} + +function advertisedLevels(entry: Record | undefined): string[] { + const rows = (entry?.supported_reasoning_levels ?? []) as { effort?: string }[]; + return rows.map(level => level.effort).filter((effort): effort is string => typeof effort === "string"); +} + +function hintedQwen(prov: OcxProviderConfig): Record { + const hinted = applyProviderConfigHints( + "alibaba-token-plan-intl", + prov, + { id: "qwen3.8-max", provider: "alibaba-token-plan-intl" }, + ); + const entries = buildCatalogEntries(nativeTemplate(), [], [hinted]); + return entries.find(e => e.slug === "alibaba-token-plan-intl/qwen3.8-max") ?? {}; +} + +describe("#1870 preserveExactReasoningRungs", () => { + test("provider-wide flag suppresses synthetic max/ultra rungs", () => { + const entry = hintedQwen(provider({ preserveExactReasoningRungs: true })); + expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh"]); + }); + + test("per-model flag scopes suppression to the named model", () => { + const entry = hintedQwen(provider({ + modelPreserveExactReasoningRungs: { "qwen3.8-max": true }, + })); + expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh"]); + }); + + test("synthetic max/ultra rungs remain advertised by default", () => { + const entry = hintedQwen(provider()); + expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]); + }); + + test("per-model false overrides a provider-wide true", () => { + const entry = hintedQwen(provider({ + preserveExactReasoningRungs: true, + modelPreserveExactReasoningRungs: { "qwen3.8-max": false }, + })); + expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]); + }); +}); From e387eaa5dce6ece78d5ccbe1fa0ebbf0662f0167 Mon Sep 17 00:00:00 2001 From: Nhat Bui Date: Mon, 17 Aug 2026 14:20:58 +0700 Subject: [PATCH 2/2] fix(catalog): clear stale preserveExactReasoningRungs flag on re-hint applyProviderConfigHints spread the incoming CatalogModel before re-adding the flag, so a cached model that previously carried preserveExactReasoningRungs: true kept it after the config changed to false. Destructure the flag out of the spread source (mirroring the supportsServiceTier pattern) and add a regression test. Closes #1870 --- .../docs/reference/configuration/providers.md | 2 +- src/codex/catalog/provider-fetch.ts | 2 +- tests/preserve-exact-reasoning-rungs.test.ts | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 0a2cdd223a..45ccbbf3d4 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -96,7 +96,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `refreshPolicy?` | `"proactive" \| "lazy-only" \| "disabled"` | Override this OAuth provider's Token Guardian policy. | | `reasoningEfforts?` | `string[]` | Provider-wide Codex reasoning labels to advertise and send. For `google`-adapter providers, a configured ladder also asserts `thinkingLevel` capability: direct and Vertex non-image requests send the selected effort as `generationConfig.thinkingConfig.thinkingLevel`, while Cloud Code Assist uses its envelope-specific path. | | `modelReasoningEfforts?` | `Record` | Per-model labels. An empty list hides effort control. As with `reasoningEfforts`, each configured `google`-adapter ladder asserts `thinkingLevel` capability; direct and Vertex non-image requests use the flat Gemini path, while Cloud Code Assist sends it under its request envelope. | -| `preserveExactReasoningRungs?` | `boolean` | Suppress the synthetic `max`/`ultra` rungs that opencodex normally appends to every reasoning-capable routed model. When `true`, the catalog advertises exactly the configured `reasoningEfforts` / `modelReasoningEfforts` ladder so the Codex picker reflects a model's real capability. Removing `max` can hard-fail subagent `spawn_agent` effort overrides, which codex-rs validates against catalog membership — enable only for models that genuinely lack the top rungs. | +| `preserveExactReasoningRungs?` | `boolean` | Suppress the synthetic `max`/`ultra` rungs that opencodex normally appends to every reasoning-capable routed model. When `true`, the catalog advertises exactly the configured `reasoningEfforts` / `modelReasoningEfforts` ladder so the Codex picker reflects a model's real capability. This only changes catalog metadata and picker display — routed request handling and adapter wire behavior are unchanged, and unsupported requested efforts are still clamped. Removing `max` can hard-fail subagent `spawn_agent` effort overrides, which codex-rs validates against catalog membership — enable only for models that genuinely lack the top rungs. | | `modelPreserveExactReasoningRungs?` | `Record` | Per-model override for `preserveExactReasoningRungs`. | | `modelSupportsReasoningSummaries?` | `Record` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. | | `modelReasoningSummaryDelivery?` | `Record` | Per-model Responses delivery enum; rewrites an existing delivery field. | diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index c763e67194..d56ad1d231 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -649,7 +649,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, ?? prov.preserveExactReasoningRungs; const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id); const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name); - const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model; + const { supportsServiceTier: _staleServiceTier, preserveExactReasoningRungs: _stalePreserveExactReasoningRungs, ...modelWithoutServiceTier } = model; const hinted = { ...modelWithoutServiceTier, ...(configuredCap !== undefined diff --git a/tests/preserve-exact-reasoning-rungs.test.ts b/tests/preserve-exact-reasoning-rungs.test.ts index df77e2a8a9..f12fe9cedc 100644 --- a/tests/preserve-exact-reasoning-rungs.test.ts +++ b/tests/preserve-exact-reasoning-rungs.test.ts @@ -69,4 +69,25 @@ describe("#1870 preserveExactReasoningRungs", () => { })); expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]); }); + + test("re-hinting a cached model clears a stale exact-rung flag", () => { + const flagged = applyProviderConfigHints( + "alibaba-token-plan-intl", + provider({ preserveExactReasoningRungs: true }), + { id: "qwen3.8-max", provider: "alibaba-token-plan-intl" }, + ); + expect(flagged.preserveExactReasoningRungs).toBe(true); + + // A cached model produced by an earlier hint pass still carries the flag; + // re-hinting with a config that resolves to false must clear it. + const cleared = applyProviderConfigHints( + "alibaba-token-plan-intl", + provider(), + flagged, + ); + expect(cleared.preserveExactReasoningRungs).toBeUndefined(); + const entries = buildCatalogEntries(nativeTemplate(), [], [cleared]); + const entry = entries.find(e => e.slug === "alibaba-token-plan-intl/qwen3.8-max"); + expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]); + }); });