diff --git a/changelog.d/fixes/codex-max-context-window.md b/changelog.d/fixes/codex-max-context-window.md new file mode 100644 index 00000000000..391d894e463 --- /dev/null +++ b/changelog.d/fixes/codex-max-context-window.md @@ -0,0 +1 @@ +- fix(codex): prefer `max_context_window` over the `context_window` pricing tier as the usable input limit in discovery, and raise the static Codex OAuth catalog to the same usable window so the conservative discovery merge no longer caps live values at the 272K pricing tier diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index d87250644dc..7e642cc4da7 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -283,14 +283,21 @@ export const GPT_5_6_API_CAPABILITIES = { maxOutputTokens: 128000, } as const; +// Codex OAuth catalog limits. The live OAuth `/codex/models` endpoint reports +// `context_window` (~272K, the first pricing tier) alongside +// `max_context_window` (~872K, the real usable window); requests past the +// pricing tier succeed upstream (verified: gpt-5.6-luna-xhigh served 380-390K +// input tokens with HTTP 200). The static catalog must advertise the usable +// window so the conservative discovery merge (`Math.min`) does not cap the +// live value at the pricing tier. export const GPT_5_6_CODEX_CAPABILITIES = { targetFormat: "openai-responses", toolCalling: true, supportsReasoning: true, supportsVision: true, supportsXHighEffort: true, - contextLength: 272000, - maxInputTokens: 272000, + contextLength: 872000, + maxInputTokens: 872000, maxOutputTokens: 128000, } as const; diff --git a/src/app/api/providers/[id]/models/discovery/codex.ts b/src/app/api/providers/[id]/models/discovery/codex.ts index 4d113863f33..7f8ec93689c 100644 --- a/src/app/api/providers/[id]/models/discovery/codex.ts +++ b/src/app/api/providers/[id]/models/discovery/codex.ts @@ -165,14 +165,19 @@ function buildCodexDiscoveryModel(record: JsonRecord): CodexDiscoveryModel | nul apiFormat: "responses", supportedEndpoints: ["responses"], }; + // The live Codex OAuth catalog reports BOTH `context_window` (the first + // pricing tier, ~272K) and `max_context_window` (the real usable window, + // ~872K). Requests well past the pricing tier succeed upstream, so the max + // window must win whenever it is present; `context_window` is only a + // fallback for catalogs that omit the max. const inputTokenLimit = firstPositiveNumber( record.inputTokenLimit, record.maxInputTokens, record.max_input_tokens, record.contextLength, record.context_length, - record.context_window, record.max_context_window, + record.context_window, topProvider.context_length, limits.input_tokens, limits.inputTokenLimit, diff --git a/tests/unit/codex-gpt56-catalog.test.ts b/tests/unit/codex-gpt56-catalog.test.ts index b4eb0ab2935..c7d8075ffb8 100644 --- a/tests/unit/codex-gpt56-catalog.test.ts +++ b/tests/unit/codex-gpt56-catalog.test.ts @@ -36,8 +36,8 @@ test("Codex catalog exposes the GPT-5.6 lineup in configured priority order", () for (const modelId of expectedIds) { const model = models.find((entry) => entry.id === modelId); assert.ok(model, `codex must expose ${modelId}`); - assert.equal(model.contextLength, 272000); - assert.equal(model.maxInputTokens, 272000); + assert.equal(model.contextLength, 872000); + assert.equal(model.maxInputTokens, 872000); assert.equal(model.maxOutputTokens, 128000); assert.equal(model.targetFormat, "openai-responses"); assert.equal(model.toolCalling, true); diff --git a/tests/unit/provider-models-discovery-split.test.ts b/tests/unit/provider-models-discovery-split.test.ts index f954c4c7f02..a98019ca649 100644 --- a/tests/unit/provider-models-discovery-split.test.ts +++ b/tests/unit/provider-models-discovery-split.test.ts @@ -272,6 +272,37 @@ test("codex.normalizeCodexModelsResponse parses the Codex live catalog shape", ( assert.equal(parsed.find((model) => model.id === "gpt-5.5")?.outputTokenLimit, 64000); }); +test("codex.normalizeCodexModelsResponse prefers max_context_window over the context_window pricing tier", () => { + // The live Codex OAuth catalog reports BOTH fields: `context_window` is the + // first pricing tier (~272K) while `max_context_window` is the real usable + // window (~872K). Requests well above 272K succeed upstream (verified: + // gpt-5.6-luna-xhigh served 380-390K input tokens with HTTP 200), so the + // usable window must win when both are present. + const parsed = normalizeCodexModelsResponse({ + models: [ + { + slug: "gpt-5.6-luna", + display_name: "GPT 5.6 Luna", + visibility: "list", + supported_in_api: true, + context_window: 272000, + max_context_window: 872000, + }, + { + slug: "gpt-5.4", + display_name: "GPT-5.4", + visibility: "list", + supported_in_api: true, + context_window: 272000, + max_context_window: 1000000, + }, + ], + }); + + assert.equal(parsed.find((model) => model.id === "gpt-5.6-luna")?.inputTokenLimit, 872000); + assert.equal(parsed.find((model) => model.id === "gpt-5.4")?.inputTokenLimit, 1000000); +}); + test("codex.normalizeCodexGithubCatalogResponse parses current client catalog metadata", () => { const parsed = normalizeCodexGithubCatalogResponse({ models: [