mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
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
This commit is contained in:
1
changelog.d/fixes/10530-codex-combo-context.md
Normal file
1
changelog.d/fixes/10530-codex-combo-context.md
Normal file
@@ -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))
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>): 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<Record<string, unknown>> };
|
||||
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",
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user