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 0000000000..391d894e46 --- /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 62909a528d..db4b9a4d5b 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -284,14 +284,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 4d113863f3..7f8ec93689 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 b4eb0ab293..c7d8075ffb 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 f954c4c7f0..a98019ca64 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: [