diff --git a/CHANGELOG.md b/CHANGELOG.md index a2dd2608ff..7a814d2629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) - **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) - **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) - **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) - **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index 2418a2bfb6..d739e09829 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -28,11 +28,17 @@ export const codexProvider: RegistryEntry = { // 1.05M). Public refs : openai/codex#19208, #19319, #19464 ; // opencode#24171. max_output_tokens is stripped server-side // (litellm#21193, codex#4138) so 128K is informational only. + // The usable INPUT budget is smaller than the 400K window (part is + // reserved for output), so max_input_tokens must be distinct from + // context_length or coding agents never auto-compact (#6191). OpenAI's + // own live catalog reports ~272K for gpt-5.5 in Codex. { id: "gpt-5.5", name: "GPT 5.5", ...GPT_5_5_CODEX_CAPABILITIES, contextLength: 400000, + // #6191: input cap per reporter; TODO confirm exact value + maxInputTokens: 272000, maxOutputTokens: 128000, }, { @@ -40,6 +46,8 @@ export const codexProvider: RegistryEntry = { name: "GPT 5.5 (xHigh)", ...GPT_5_5_CODEX_CAPABILITIES, contextLength: 400000, + // #6191: input cap per reporter; TODO confirm exact value + maxInputTokens: 272000, maxOutputTokens: 128000, }, { @@ -47,6 +55,8 @@ export const codexProvider: RegistryEntry = { name: "GPT 5.5 (High)", ...GPT_5_5_CODEX_CAPABILITIES, contextLength: 400000, + // #6191: input cap per reporter; TODO confirm exact value + maxInputTokens: 272000, maxOutputTokens: 128000, }, { @@ -54,6 +64,8 @@ export const codexProvider: RegistryEntry = { name: "GPT 5.5 (Medium)", ...GPT_5_5_CODEX_CAPABILITIES, contextLength: 400000, + // #6191: input cap per reporter; TODO confirm exact value + maxInputTokens: 272000, maxOutputTokens: 128000, }, { @@ -61,6 +73,8 @@ export const codexProvider: RegistryEntry = { name: "GPT 5.5 (Low)", ...GPT_5_5_CODEX_CAPABILITIES, contextLength: 400000, + // #6191: input cap per reporter; TODO confirm exact value + maxInputTokens: 272000, maxOutputTokens: 128000, }, { diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index ecc4fd088d..1a90d258e6 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -56,6 +56,13 @@ export interface RegistryModel { unsupportedParams?: readonly string[]; /** Maximum context window in tokens */ contextLength?: number; + /** + * Explicit maximum input-token budget, when it is smaller than the full + * context window (e.g. OAuth backends that reserve part of the window for + * output). When set, catalog/capability builders prefer this over deriving + * max_input_tokens from contextLength (#6191). + */ + maxInputTokens?: number; /** * Interleaved-reasoning signal, mirroring models.dev's `interleaved_field`. * Set to "reasoning_content" for models whose upstream runs DeepSeek thinking diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 664cc0087a..73733107af 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -292,9 +292,13 @@ export async function getUnifiedModelsResponse( registryContext ?? specContext ?? (getTokenLimit(providerId, modelId) || undefined); - const maxInputTokens = isPositiveFiniteNumber(synced?.limit_input) + const registryInputLimit = isPositiveFiniteNumber(registryModel?.maxInputTokens) + ? registryModel.maxInputTokens + : undefined; + const syncedInputLimit = isPositiveFiniteNumber(synced?.limit_input) ? synced.limit_input - : contextLength; + : undefined; + const maxInputTokens = registryInputLimit ?? syncedInputLimit ?? contextLength; const maxOutputTokens = isPositiveFiniteNumber(synced?.limit_output) ? synced.limit_output : isPositiveFiniteNumber(spec?.maxOutputTokens) diff --git a/src/lib/modelCapabilities.ts b/src/lib/modelCapabilities.ts index 866d073643..cae1173042 100644 --- a/src/lib/modelCapabilities.ts +++ b/src/lib/modelCapabilities.ts @@ -406,7 +406,11 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo structuredOutput: synced?.structured_output ?? null, temperature: synced?.temperature ?? null, contextWindow, - maxInputTokens: authoritativeContextWindow ?? synced?.limit_input ?? contextWindow, + maxInputTokens: + (typeof registryModel?.maxInputTokens === "number" ? registryModel.maxInputTokens : null) ?? + authoritativeContextWindow ?? + synced?.limit_input ?? + contextWindow, maxOutputTokens: synced?.limit_output ?? (typeof registryModel?.maxOutputTokens === "number" ? registryModel.maxOutputTokens : null) ?? diff --git a/tests/unit/gpt-max-input-tokens-6191.test.ts b/tests/unit/gpt-max-input-tokens-6191.test.ts new file mode 100644 index 0000000000..f89c42edb1 --- /dev/null +++ b/tests/unit/gpt-max-input-tokens-6191.test.ts @@ -0,0 +1,67 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// #6191: GPT-family codex models advertised their full 400K context window as +// BOTH context_length and max_input_tokens, so coding agents never triggered +// auto-compaction. The real usable input budget is smaller (~272K). These tests +// pin the distinct-input-cap behavior and guard the contextLength fallback for +// models that do NOT declare an explicit maxInputTokens. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-gpt-input-cap-6191-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelCapabilities = await import("../../src/lib/modelCapabilities.ts"); + +function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(() => { + resetStorage(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("codex gpt-5.5 reports max_input_tokens smaller than its context window (#6191)", () => { + const caps = modelCapabilities.getResolvedModelCapabilities("codex/gpt-5.5"); + assert.equal(caps.contextWindow, 400000); + assert.equal(caps.maxInputTokens, 272000); + assert.ok( + (caps.maxInputTokens ?? 0) < (caps.contextWindow ?? 0), + "max_input_tokens must be strictly smaller than context_length so agents compact" + ); +}); + +test("all codex gpt-5.5 effort variants carry the distinct input cap (#6191)", () => { + for (const modelId of [ + "codex/gpt-5.5-xhigh", + "codex/gpt-5.5-high", + "codex/gpt-5.5-medium", + "codex/gpt-5.5-low", + ]) { + const caps = modelCapabilities.getResolvedModelCapabilities(modelId); + assert.equal(caps.contextWindow, 400000, modelId); + assert.equal(caps.maxInputTokens, 272000, modelId); + } +}); + +test("regression: a model without maxInputTokens still falls back to its context window", () => { + // codex gpt-5.4 declares no maxInputTokens, so max_input_tokens must equal + // the context window (the historical fallback) — no under-reporting. + const caps = modelCapabilities.getResolvedModelCapabilities("codex/gpt-5.4"); + assert.ok((caps.contextWindow ?? 0) > 0, "gpt-5.4 should have a context window"); + assert.equal( + caps.maxInputTokens, + caps.contextWindow, + "without an explicit input cap, max_input_tokens falls back to context_length" + ); +}); diff --git a/tests/unit/model-capabilities-registry.test.ts b/tests/unit/model-capabilities-registry.test.ts index b6582a1ca6..c4be2f0e49 100644 --- a/tests/unit/model-capabilities-registry.test.ts +++ b/tests/unit/model-capabilities-registry.test.ts @@ -108,7 +108,8 @@ test("canonical model capability resolver lets exact synced metadata override gl const codexGpt55 = modelCapabilities.getResolvedModelCapabilities("codex/gpt-5.5"); assert.equal(codexGpt55.contextWindow, 400000); - assert.equal(codexGpt55.maxInputTokens, 400000); + // #6191: max_input_tokens is a distinct, smaller cap than the context window. + assert.equal(codexGpt55.maxInputTokens, 272000); assert.equal(codexGpt55.maxOutputTokens, 128000); assert.equal(codexGpt55.supportsThinking, true); assert.equal(codexGpt55.supportsVision, true);