fix(catalog): auto-calculate combo context_length from target model limits (#2030)

Integrated into release/v3.8.0
This commit is contained in:
Hernan Javier Ardila Sanchez
2026-05-07 13:48:52 +02:00
committed by GitHub
parent a7e00fbe4a
commit 84955146d0
3 changed files with 84 additions and 4 deletions

6
package-lock.json generated
View File

@@ -9437,9 +9437,9 @@
"license": "MIT"
},
"node_modules/hono": {
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"version": "4.12.18",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"

View File

@@ -25,6 +25,8 @@ import {
getCatalogDiagnosticsHeaders,
} from "@/lib/modelMetadataRegistry";
import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth";
import { parseModel } from "@omniroute/open-sse/services/model.ts";
import { getTokenLimit } from "@omniroute/open-sse/services/contextManager.ts";
const FALLBACK_ALIAS_TO_PROVIDER = {
ag: "antigravity",
@@ -313,6 +315,30 @@ export async function getUnifiedModelsResponse(
// Add combos first (they appear at the top) — only active ones
for (const combo of combos) {
if (combo.isActive === false || combo.isHidden === true) continue;
// Calculate combo context length from its model targets.
// OpenCode and other clients read context_length from the catalog; without it
// they fall back to a conservative ~4000 token limit, causing truncation.
const comboContextLength = Array.isArray(combo.models)
? combo.models
.filter((step) => step && step.kind === "model" && step.model)
.map((step) => {
const parsed = parseModel(step.model);
const provider = parsed.provider || (step as any).providerId || "unknown";
const model = parsed.model || step.model;
return getTokenLimit(provider, model);
})
.filter((limit): limit is number => typeof limit === "number" && limit > 0)
.reduce((min, limit) => Math.min(min, limit), Infinity)
: undefined;
const effectiveContextLength =
typeof combo.context_length === "number" && combo.context_length > 0
? combo.context_length
: comboContextLength !== undefined && comboContextLength !== Infinity
? comboContextLength
: undefined;
models.push({
id: combo.name,
object: "model",
@@ -321,7 +347,7 @@ export async function getUnifiedModelsResponse(
permission: [],
root: combo.name,
parent: null,
...(combo.context_length ? { context_length: combo.context_length } : {}),
...(effectiveContextLength !== undefined ? { context_length: effectiveContextLength } : {}),
});
}

View File

@@ -892,3 +892,57 @@ test("v1 models catalog adds managed fallback models for Claude-compatible provi
assert.ok(ids.has("ccdemo/claude-opus-4-6"));
assert.equal(ids.has("ccdemo/claude-sonnet-4-6"), false);
});
test("v1 models catalog auto-calculates combo context_length from targets when not set manually", async () => {
await seedConnection("openai", { name: "openai-auto-context" });
await seedConnection("claude", {
authType: "oauth",
name: "claude-auto-context",
apiKey: null,
accessToken: "claude-access",
});
// Create a combo with targets having different context limits.
// openai/gpt-4o context = 128000, claude/claude-sonnet-4-6 = 200000.
// The combo should expose context_length = min = 128000.
const combo = await combosDb.createCombo({
name: "auto-context-combo",
strategy: "priority",
models: ["openai/gpt-4o", "claude/claude-sonnet-4-6"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const comboModel = body.data.find((item) => item.id === "auto-context-combo");
assert.equal(response.status, 200);
assert.ok(comboModel);
assert.equal(
comboModel.context_length,
128000,
"combo context_length should be the MIN of all target model limits"
);
});
test("v1 models catalog prefers manual combo context_length over auto-calculated", async () => {
await seedConnection("openai", { name: "openai-manual-context" });
const combo = await combosDb.createCombo({
name: "manual-context-combo",
strategy: "priority",
models: ["openai/gpt-4o"],
});
await combosDb.updateCombo((combo as any).id, { context_length: 64000 });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const comboModel = body.data.find((item) => item.id === "manual-context-combo");
assert.equal(response.status, 200);
assert.ok(comboModel);
assert.equal(comboModel.context_length, 64000, "manual context_length should override auto-calc");
});