From 9a433775e7d5cb3d1b7a8f961783aa5c1ed2db5d Mon Sep 17 00:00:00 2001 From: Ke Jin Date: Tue, 18 Aug 2026 21:51:42 +0800 Subject: [PATCH] fix(models): correct Codex context and combo limit resolution (#10533) * fix(models): honor Codex combo context overrides * test(codex): align discovery context expectation * test(models): align Codex route limits * test(models): align remaining Codex route limits --- .../fixes/10530-codex-combo-context.md | 1 + open-sse/config/providers/shared.ts | 4 +- src/app/api/v1/models/catalog.ts | 27 +++-------- src/lib/combos/comboContext.ts | 47 +++++-------------- tests/unit/codex-gpt56-catalog.test.ts | 4 +- .../combo-context-prefix-resolution.test.ts | 16 +++++++ .../models-catalog-combo-metadata.test.ts | 38 +++++++++++++++ .../unit/provider-models-route-codex.test.ts | 7 ++- tests/unit/vscode-token-routes-gpt56.test.ts | 4 +- tests/unit/vscode-token-routes.test.ts | 6 +-- 10 files changed, 84 insertions(+), 70 deletions(-) create mode 100644 changelog.d/fixes/10530-codex-combo-context.md diff --git a/changelog.d/fixes/10530-codex-combo-context.md b/changelog.d/fixes/10530-codex-combo-context.md new file mode 100644 index 0000000000..29ada2699a --- /dev/null +++ b/changelog.d/fixes/10530-codex-combo-context.md @@ -0,0 +1 @@ +- **fix(models):** align Codex GPT-5.6 context limits with the Codex catalog and honor model context overrides when advertising combos ([#10530](https://github.com/diegosouzapw/OmniRoute/issues/10530)) diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 994b4830b0..88fff0fef7 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -279,8 +279,8 @@ export const GPT_5_6_CODEX_CAPABILITIES = { supportsReasoning: true, supportsVision: true, supportsXHighEffort: true, - contextLength: 1050000, - maxInputTokens: 922000, + contextLength: 272000, + maxInputTokens: 272000, maxOutputTokens: 128000, } as const; diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 3d1b33f5e9..d690d6a515 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -431,27 +431,12 @@ async function buildUnifiedModelsResponseCore( const syncedInputModalities = parseJsonStringArray(synced?.modalities_input); const syncedOutputModalities = parseJsonStringArray(synced?.modalities_output); - const syncedContext = isPositiveFiniteNumber(synced?.limit_context) - ? synced.limit_context - : undefined; - const registryContext = isPositiveFiniteNumber(registryModel?.contextLength) - ? registryModel.contextLength - : undefined; - const specContext = isPositiveFiniteNumber(spec?.contextWindow) - ? spec.contextWindow - : undefined; - const contextLength = - syncedContext ?? - registryContext ?? - specContext ?? - (getTokenLimit(providerId, modelId) || undefined); - const registryInputLimit = isPositiveFiniteNumber(registryModel?.maxInputTokens) - ? registryModel.maxInputTokens - : undefined; - const syncedInputLimit = isPositiveFiniteNumber(synced?.limit_input) - ? synced.limit_input - : undefined; - const maxInputTokens = registryInputLimit ?? syncedInputLimit ?? contextLength; + const contextLength = isPositiveFiniteNumber(canonical.limits.contextWindow) + ? canonical.limits.contextWindow + : getTokenLimit(providerId, modelId) || undefined; + const maxInputTokens = isPositiveFiniteNumber(canonical.limits.maxInputTokens) + ? canonical.limits.maxInputTokens + : contextLength; const maxOutputTokens = isPositiveFiniteNumber(synced?.limit_output) ? synced.limit_output : isPositiveFiniteNumber(spec?.maxOutputTokens) diff --git a/src/lib/combos/comboContext.ts b/src/lib/combos/comboContext.ts index 8d5e492cdf..0c7744cc41 100644 --- a/src/lib/combos/comboContext.ts +++ b/src/lib/combos/comboContext.ts @@ -1,9 +1,8 @@ /** * Combo context-length computation. * - * Computes the effective context_window for a combo using the same resolution - * chain as the catalog's `getComboTargetCatalogMetadata`: - * synced → registry → spec → getTokenLimit + * Computes the effective context_window for a combo from canonical model + * metadata, which already applies persisted overrides and source precedence. * * Only models that are registered in at least one data source (provider registry, * static specs, or synced capabilities) contribute to the result — matching the @@ -12,9 +11,6 @@ import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo"; import { getCanonicalModelMetadata } from "@/lib/modelMetadataRegistry"; -import { getSyncedCapability } from "@/lib/modelsDevSync"; -import { getModelSpec } from "@/shared/constants/modelSpecs"; -import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { getTokenLimit } from "@omniroute/open-sse/services/contextManager"; import { buildAliasMaps, getComboTargetModelId } from "@/app/api/v1/models/catalogProviderMaps"; @@ -30,17 +26,6 @@ function minKnownNumber(values: Array): number | undefined { return known.length > 0 ? Math.min(...known) : undefined; } -/** Look up a model in the provider-registry model list. */ -function getRegistryModel( - providerId: string, - modelId: string -): { contextLength?: number; id?: string; name?: string } | null { - const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; - const providerModels: Array<{ id?: string; contextLength?: number }> = - PROVIDER_MODELS[alias] || PROVIDER_MODELS[providerId] || []; - return providerModels.find((m) => m?.id === modelId) ?? null; -} - /* ─── public API ────────────────────────────────────────────── */ /** @@ -48,9 +33,9 @@ function getRegistryModel( * * Resolution order: * 1. Explicit `context_length` on the combo record itself. - * 2. Minimum of member-model context windows — each member resolved via - * synced → registry → spec → getTokenLimit, only counting members that - * exist in at least one known data source (matching the catalog behavior). + * 2. Minimum of member-model effective context windows from canonical metadata, + * only counting members that exist in at least one known data source + * (matching the catalog behavior). * * Returns `undefined` when no known context window can be determined. */ @@ -75,8 +60,8 @@ export function computeComboContextLength( if (!Array.isArray(targets) || targets.length === 0) return undefined; - // 3. Per-target context resolution — same logic as the catalog's - // `getComboTargetCatalogMetadata`. + // 3. Per-target context resolution from canonical metadata, matching the + // catalog's `getComboTargetCatalogMetadata`. const contextValues: number[] = []; const aliasMaps = buildAliasMaps(); @@ -111,20 +96,10 @@ export function computeComboContextLength( const providerId = canonicalMeta.provider || resolvedTarget.providerId; const modelId = canonicalMeta.model || resolvedTarget.modelId; - // 3c. Resolve window: synced → registry → spec → getTokenLimit - const synced = getSyncedCapability(providerId, modelId); - const spec = getModelSpec(modelId); - const registryModel = getRegistryModel(providerId, modelId); - - const syncedCtx = isPositiveFiniteNumber(synced?.limit_context) - ? (synced.limit_context as number) - : undefined; - const registryCtx = isPositiveFiniteNumber(registryModel?.contextLength) - ? registryModel.contextLength - : undefined; - const specCtx = isPositiveFiniteNumber(spec?.contextWindow) ? spec.contextWindow : undefined; - - const targetCtx = syncedCtx ?? registryCtx ?? specCtx ?? getTokenLimit(providerId, modelId); + const targetCtx = + (isPositiveFiniteNumber(canonicalMeta.limits.contextWindow) + ? canonicalMeta.limits.contextWindow + : undefined) ?? getTokenLimit(providerId, modelId); if (isPositiveFiniteNumber(targetCtx)) { contextValues.push(targetCtx); diff --git a/tests/unit/codex-gpt56-catalog.test.ts b/tests/unit/codex-gpt56-catalog.test.ts index 8cdfbe4dd7..b4eb0ab293 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, 1050000); - assert.equal(model.maxInputTokens, 922000); + assert.equal(model.contextLength, 272000); + assert.equal(model.maxInputTokens, 272000); assert.equal(model.maxOutputTokens, 128000); assert.equal(model.targetFormat, "openai-responses"); assert.equal(model.toolCalling, true); diff --git a/tests/unit/combo-context-prefix-resolution.test.ts b/tests/unit/combo-context-prefix-resolution.test.ts index 05b4612347..bd586f2efc 100644 --- a/tests/unit/combo-context-prefix-resolution.test.ts +++ b/tests/unit/combo-context-prefix-resolution.test.ts @@ -33,6 +33,8 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const { computeComboContextLength } = await import("../../src/lib/combos/comboContext.ts"); +const { setModelContextOverride, removeModelContextOverride } = + await import("../../src/lib/db/modelContextOverrides.ts"); test.after(() => { core.resetDbInstance(); @@ -72,3 +74,17 @@ test("computeComboContextLength takes the minimum across multiple prefixed, regi "matching the catalog's minKnownNumber semantics" ); }); + +test("computeComboContextLength honors a larger persisted Codex GPT-5.6 window", () => { + const modelId = "gpt-5.6-terra"; + assert.equal(setModelContextOverride("codex", modelId, 500000, "manual"), true); + try { + assert.equal( + computeComboContextLength({ models: [`codex/${modelId}`] }, []), + 500000, + "the combo aggregate must use the effective override instead of the Codex registry default" + ); + } finally { + removeModelContextOverride("codex", modelId); + } +}); diff --git a/tests/unit/models-catalog-combo-metadata.test.ts b/tests/unit/models-catalog-combo-metadata.test.ts index e262bf4971..017faca297 100644 --- a/tests/unit/models-catalog-combo-metadata.test.ts +++ b/tests/unit/models-catalog-combo-metadata.test.ts @@ -11,6 +11,7 @@ process.env.API_KEY_SECRET ||= "combo-metadata-test-secret"; const core = await import("../../src/lib/db/core.ts"); const providersDb = await import("../../src/lib/db/providers.ts"); const combosDb = await import("../../src/lib/db/combos.ts"); +const contextOverrides = await import("../../src/lib/db/modelContextOverrides.ts"); const catalog = await import("../../src/app/api/v1/models/catalog.ts"); test.after(() => { @@ -56,6 +57,43 @@ test("single-target combo preserves its direct model metadata", async () => { } }); +test("single-target Codex combo advertises a larger model context override", async () => { + const modelId = "gpt-5.6-terra"; + const contextWindow = 500000; + assert.equal(contextOverrides.setModelContextOverride("codex", modelId, contextWindow), true); + + try { + await providersDb.createProviderConnection({ + provider: "codex", + authType: "oauth", + name: "codex-gpt-5.6-context-override-combo", + accessToken: "codex-test-token", + isActive: true, + testStatus: "active", + providerSpecificData: {}, + }); + await combosDb.createCombo({ + name: "gpt-5.6-context-override-combo", + strategy: "auto", + models: [`codex/${modelId}`], + }); + + const response = await catalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as { data: Array> }; + const direct = body.data.find((item) => item.id === `cx/${modelId}`); + const combo = body.data.find((item) => item.id === "gpt-5.6-context-override-combo"); + + assert.equal(response.status, 200); + assert.equal(direct?.context_length, contextWindow); + assert.equal(combo?.context_length, contextWindow); + assert.equal(combo?.max_input_tokens, 272000); + } finally { + contextOverrides.removeModelContextOverride("codex", modelId); + } +}); + test("single-target combo respects registry reasoning overrides before specs", async () => { await providersDb.createProviderConnection({ provider: "command-code", diff --git a/tests/unit/provider-models-route-codex.test.ts b/tests/unit/provider-models-route-codex.test.ts index daf0f5c4c2..890853c14c 100644 --- a/tests/unit/provider-models-route-codex.test.ts +++ b/tests/unit/provider-models-route-codex.test.ts @@ -181,11 +181,10 @@ test("provider models route merges live Codex models with the local catalog then // merge conservatively — the smaller of live vs. pinned wins, never the // larger, so a stale/inflated live number can never make OmniRoute promise // more context than the account can actually serve (#7012). Here the pinned - // GPT-5.6 Codex contract (922000/128000, see GPT_5_6_CODEX_CAPABILITIES — - // raised from 272000 in #9432) is smaller than the live payload's - // 999999/999999, so the pinned value wins. + // GPT-5.6 Codex contract (272000/128000, see GPT_5_6_CODEX_CAPABILITIES) + // is smaller than the live payload's 999999/999999, so the pinned value wins. assert.equal(liveModel?.name, "GPT 5.6 Sol Live"); - assert.equal(liveModel?.inputTokenLimit, 922000); + assert.equal(liveModel?.inputTokenLimit, 272000); assert.equal(liveModel?.outputTokenLimit, 128000); assert.equal(liveModel?.apiFormat, "responses"); assert.deepEqual(liveModel?.supportedEndpoints, ["responses"]); diff --git a/tests/unit/vscode-token-routes-gpt56.test.ts b/tests/unit/vscode-token-routes-gpt56.test.ts index 8fd76bb92c..c3e33f006b 100644 --- a/tests/unit/vscode-token-routes-gpt56.test.ts +++ b/tests/unit/vscode-token-routes-gpt56.test.ts @@ -128,9 +128,9 @@ test("vscode raw models route exposes native GPT-5.6 IDs and effort tiers", asyn assert.equal(typeof defaultModel.created, "number"); assert.equal(defaultModel.owned_by, "codex"); assert.equal(defaultModel.name, "Codex GPT 5.6 Sol"); - assert.equal(defaultModel.context_length, 1050000); + assert.equal(defaultModel.context_length, 272000); assert.equal(defaultModel.max_output_tokens, 128000); - assert.equal(defaultModel.max_input_tokens, 922000); + assert.equal(defaultModel.max_input_tokens, 272000); assert.deepEqual(defaultModel.capabilities, { vision: true, tool_calling: true, diff --git a/tests/unit/vscode-token-routes.test.ts b/tests/unit/vscode-token-routes.test.ts index cc82ffb061..fe7fbf64d7 100644 --- a/tests/unit/vscode-token-routes.test.ts +++ b/tests/unit/vscode-token-routes.test.ts @@ -255,7 +255,7 @@ test("vscode combos route resolves combo names through Ollama api/show", async ( assert.equal(body.model, "show-combo"); assert.equal(body.modelfile, "FROM show-combo"); assert.equal(body.details.family, "show-combo"); - assert.equal(body.model_info.context_length, 1050000); + assert.equal(body.model_info.context_length, 272000); assert.deepEqual(body.supportsReasoningEffort, ["none", "low", "medium", "high", "xhigh"]); assert.equal(body.model_info.capabilities.reasoning, true); }); @@ -290,7 +290,7 @@ test("vscode tokenized combos root route exposes importable combo metadata", asy assert.equal(response.status, 200); assert.ok(combo, "expected balanced-load in combo root response"); assert.equal(combo.url.includes("/responses#models.ai.azure.com"), true); - assert.equal(combo.maxInputTokens, 922000); + assert.equal(combo.maxInputTokens, 272000); assert.equal(combo.toolCalling, true); assert.deepEqual(combo.supportsReasoningEffort, ["none", "low", "medium", "high", "xhigh"]); }); @@ -1073,7 +1073,7 @@ test("vscode tokenized api/show route exposes explicit reasoning effort metadata assert.equal(body.configurationSchema?.properties?.reasoningEffort?.default, "low"); assert.equal(body.model_info["general.basename"], "Codex GPT 5.6 Sol (Default)"); assert.equal(body.model_info["general.architecture"], "codex"); - assert.equal(body.model_info["codex.context_length"], 1050000); + assert.equal(body.model_info["codex.context_length"], 272000); assert.deepEqual(body.model_info.supports_reasoning_effort, [ "low", "medium",