fix(backend): distinct max_input_tokens for GPT-family models (#6191) (#6230)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-04 23:02:55 -03:00
committed by GitHub
parent adde9e4bae
commit 1473261c4c
7 changed files with 102 additions and 4 deletions

View File

@@ -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 `<mangled>` 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`/`<think>` 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`.

View File

@@ -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,
},
{

View File

@@ -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

View File

@@ -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)

View File

@@ -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) ??

View File

@@ -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"
);
});

View File

@@ -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);