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

Fixes the root cause where OpenCode falls back to a ~4000 token limit
for combos because no context_length is exposed in /v1/models.

Previously combos only used context_length when set manually on the
combo record. Now, when unset, the catalog computes the effective
limit as the MINIMUM of its targets' individual token limits via
getTokenLimit()/parseModel(). Manual values still override.

Files changed:
- src/app/api/v1/models/catalog.ts  (+30 lines, auto-calc)
- tests/unit/models-catalog-route.test.ts  (+2 tests)

Tests pass: 25/25
This commit is contained in:
Automation
2026-05-07 10:36:16 +02:00
parent 08e18867fd
commit 3dc7542eca
2 changed files with 81 additions and 1 deletions

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