Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/0000-combo-sibling-specific-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- fix(context): combo members whose window resolves only to the generic 128000 catch-all no longer clamp the runtime combo context limit — the sibling-side counterpart of #10734 (#14931 — thanks @skygunner)
12 changes: 10 additions & 2 deletions open-sse/services/contextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,22 @@ export function getSourcedTokenLimit(
*
* `ResolvedComboTarget.provider` is populated independently of `modelStr`, so fall
* back to it before calling `getTokenLimit` (#8716).
*
* Known sources only (#14931): a target whose window resolves solely to the
* generic catch-all default returns `undefined` instead of that guess, so the
* runtime combo `Math.min()` in `resolveComboContextLimit` drops it — the
* sibling-side counterpart of the #10734 rule `getSourcedTokenLimit` already
* applies to advertised combos. An uncataloged member must not clamp the whole
* combo to a 128000 it may not have; when every member is unknown the resolver
* still falls back to the generic default on its own (source=fallback).
*/
export function getComboTargetTokenLimit(options: {
modelStr?: string | null;
provider?: string | null;
parsedProvider?: string | null;
parsedModel?: string | null;
targetProvider?: string | null;
}): number {
}): number | undefined {
let parsedProvider = options.parsedProvider;
let parsedModel = options.parsedModel;
if (
Expand All @@ -385,7 +393,7 @@ export function getComboTargetTokenLimit(options: {
if (parsedModel === undefined) parsedModel = parsed.model;
}
const provider = parsedProvider ?? options.targetProvider ?? options.provider ?? "unknown";
return getTokenLimit(provider, parsedModel ?? null);
return getSourcedTokenLimit(provider, parsedModel ?? null);
}

/**
Expand Down
151 changes: 151 additions & 0 deletions tests/unit/chatcore-combo-sibling-specific-14931.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Regression for #14931 (limit side): an UNCATALOGED combo member must not
// clamp the whole combo's runtime context limit to the generic 128000
// catch-all. chatCore builds comboTargetLimits via getComboTargetTokenLimit(),
// which now returns undefined for targets whose window resolves only from the
// non-specific default (the sibling-side counterpart of #10734's
// getSourcedTokenLimit rule). The executing member itself being uncataloged
// therefore inherits min(known sibling windows) — here 1,000,000 — instead of
// the baseless 128000 that false-killed ~50k-real-token Codex requests.
//
// Two cases against the same combo [unknown member, 1M env-declared sibling]:
// A) ~150k estimated tokens: OLD code 400s at 128000; NEW code passes the
// guard and succeeds against the stubbed upstream.
// B) ~1.1M estimated tokens: still rejected, citing the inherited 1,000,000
// (the guard keeps its teeth — inheritance is a better estimate, not the
// removal of the check).
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";

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-sibling-14931-"));
process.env.DATA_DIR = TEST_DATA_DIR;

const core = await import("../../src/lib/db/core.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts");

// Provider/model names chosen to match no registry entry and no name-family
// heuristic in resolveTokenLimit(), so the unknown member resolves ONLY to the
// generic 128000 (specific=false).
const UNKNOWN_PROVIDER = "c14931-unkprov";
const UNKNOWN_MODEL = "c14931-unkmodel";
// Known sibling: env override is the highest-priority specific source.
const KNOWN_PROVIDER = "c14931-knownprov";
const KNOWN_MODEL = "c14931-knownmodel";
const KNOWN_LIMIT_ENV = "CONTEXT_LENGTH_C14931_KNOWNPROV";
const KNOWN_LIMIT = 1_000_000;
const COMBO_NAME = "c14931-test-combo";

const originalFetch = globalThis.fetch;
const originalKnownEnv = process.env[KNOWN_LIMIT_ENV];

async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}

function buildRequest(content: string) {
return {
body: {
model: UNKNOWN_MODEL,
messages: [{ role: "user", content }],
stream: false,
},
modelInfo: {
provider: UNKNOWN_PROVIDER,
model: UNKNOWN_MODEL,
extendedContext: false,
},
credentials: {
apiKey: "sk-test",
providerSpecificData: { baseUrl: "https://c14931.example.test" },
},
clientRawRequest: {
endpoint: "/v1/chat/completions",
body: {
model: UNKNOWN_MODEL,
messages: [{ role: "user", content }],
stream: false,
},
headers: new Headers({ accept: "application/json" }),
},
userAgent: "unit-test",
isCombo: true,
comboName: COMBO_NAME,
log: {
debug() {},
info() {},
warn() {},
error() {},
},
};
}

test.before(async () => {
await resetStorage();
process.env[KNOWN_LIMIT_ENV] = String(KNOWN_LIMIT);
await combosDb.createCombo({
name: COMBO_NAME,
models: [`${UNKNOWN_PROVIDER}/${UNKNOWN_MODEL}`, `${KNOWN_PROVIDER}/${KNOWN_MODEL}`],
});
globalThis.fetch = async () =>
new Response(JSON.stringify({ choices: [{ message: { content: "stub ok" } }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
});

test.after(() => {
globalThis.fetch = originalFetch;
if (originalKnownEnv === undefined) {
delete process.env[KNOWN_LIMIT_ENV];
} else {
process.env[KNOWN_LIMIT_ENV] = originalKnownEnv;
}
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});

test("#14931 A: uncataloged executing member no longer clamped to generic 128000 by itself", async () => {
// 600k chars → ~150k estimated tokens: above the old 128000 combo-min, far
// below the inherited 1,000,000. Old code: 400 "limit 128000". New code: the
// guard passes — the stubbed upstream answers, and even if the leg later
// fails for unrelated executor reasons, the failure must NOT be a
// context-length rejection.
const content = "x".repeat(600_000);
const result = await handleChatCore(buildRequest(content));
if (result.success) {
assert.ok(true, "guard passed and the stubbed upstream answered");
return;
}
const failure = result as { success: false; error: string; rawMessage?: string };
const message = failure.rawMessage ?? failure.error;
assert.doesNotMatch(
message,
/context_length_exceeded|exceeds (maximum input tokens|context window)/i,
`the request must not be rejected by the context guard; got: ${message}`
);
assert.doesNotMatch(message, /limit 128000\b/, `got: ${message}`);
});

test("#14931 B: oversized request still rejected against the inherited sibling limit", async () => {
// 4.6M chars → ~1.15M estimated tokens: above even the inherited window.
const content = "x".repeat(4_600_000);
const result = await handleChatCore(buildRequest(content));
assert.equal(result.success, false, "expected the oversized request to be rejected");
const failure = result as { success: false; error: string; rawMessage?: string };
const message = failure.rawMessage ?? failure.error;
assert.match(
message,
/limit 1000000\b/,
`expected the rejection to cite the inherited 1,000,000 limit; got: ${message}`
);
assert.doesNotMatch(
message,
/limit 128000\b/,
"the generic 128000 must not be the cited limit once a known sibling exists"
);
});
30 changes: 28 additions & 2 deletions tests/unit/combo-target-token-limit-8716.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,39 @@ test("#8716 getComboTargetTokenLimit prefers parseModel provider when present",
assert.equal(withPrefix, fromParsedOnly);
});

test("#8716 getComboTargetTokenLimit uses unknown when both providers missing", () => {
test("#8716/#14931 getComboTargetTokenLimit returns undefined when only the generic default resolves", () => {
// Both providers missing → "unknown" provider + uncataloged model resolves
// solely to the non-specific 128000 catch-all. #14931: that guess must NOT
// enter the runtime combo Math.min(); the resolver falls back to it on its
// own only when every member is unknown.
const limit = getComboTargetTokenLimit({
parsedProvider: null,
parsedModel: "some-model",
targetProvider: null,
});
assert.ok(Number.isFinite(limit) && limit > 0);
assert.equal(limit, undefined);
});

test("#14931 specific sources still resolve (name heuristic is a known source)", () => {
// Step 4 of resolveTokenLimit (model-name heuristic) runs regardless of
// provider known-ness: an uncataloged "gpt*" variant on an unknown provider
// still resolves specific, while a non-matching name stays unknown.
const heuristic = getComboTargetTokenLimit({
parsedProvider: "totally-unknown-provider",
parsedModel: "gpt-4o-unknown-variant",
targetProvider: null,
});
assert.ok(Number.isFinite(heuristic) && (heuristic as number) > 0);

// The #14931 incident shape: uncataloged model behind a custom
// OpenAI-compatible connection has no catalog row, no registry entry and a
// name that matches no heuristic → unknown, must not fake 128000.
const unknownProvider = getComboTargetTokenLimit({
parsedProvider: "openai-compatible-chat-d9363e0c",
parsedModel: "qwen3.8-27b",
targetProvider: null,
});
assert.equal(unknownProvider, undefined);
});

test("#8716 chatCore combo-limit map uses getComboTargetTokenLimit (source guard)", async () => {
Expand Down