From 3f30ea18bf38f0569e8eb51d55bf8f15be375749 Mon Sep 17 00:00:00 2001 From: Herjarsa Date: Sat, 6 Jun 2026 16:10:36 +0200 Subject: [PATCH 1/4] fix(opencode-provider): extract contextLength from live model catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept OmniRouteLiveModel[] in createOmniRouteProvider models option, extracting contextLength directly from the live API catalog (/v1/models) instead of relying on a hardcoded static map. Previously only 8 Claude/Gemini models had entries in OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS; any other model (e.g. deepseek-v4-flash 1M, kimi-k2.6 262K, qwen-2.5-coder 200K) got undefined contextLength, causing OpenCode to fall back to its internal 128K default — silently truncating context windows. Priority: live model entry > modelContextLengths option > static defaults. Fixes silent truncation for all passthrough models not in the legacy map. --- @omniroute/opencode-provider/src/index.ts | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index 6f3a2384eee0..258c52c5afa5 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -129,8 +129,8 @@ export interface OmniRouteProviderOptions { apiKey: string; /** Override the display name shown in OpenCode. Default: `"OmniRoute"`. */ displayName?: string; - /** Override the model catalog. Defaults to `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. */ - models?: readonly string[]; + /** Override the model catalog. Accepts model ids (strings) or live model entries from `fetchLiveModels`. When entries carry a `contextLength`, it is used directly — no hardcoded map needed. */ + models?: readonly (string | { id: string; contextLength?: number })[]; /** Optional human-readable labels keyed by model id. Overridden by `modelCapabilities[id].label`. */ modelLabels?: Record; /** @@ -138,7 +138,13 @@ export interface OmniRouteProviderOptions { * `OMNIROUTE_DEFAULT_MODEL_CAPABILITIES` for ids in the default catalog; * for custom ids the override is used verbatim. */ - modelCapabilities?: Record; +modelCapabilities?: Record; + /** + * Optional per-model context-length overrides (tokens). Takes precedence + * over the static `OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS` map but is + * superseded by `contextLength` on live model entries passed via `models`. + */ + modelContextLengths?: Record; /** * Primary model for OpenCode (top-level `model` key). * Emitted as `"omniroute/"`. When omitted the key is not written. @@ -248,7 +254,12 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open const models: Record = {}; const seen = new Set(); for (const raw of modelList) { - const id = typeof raw === "string" ? raw.trim() : ""; + const id = + typeof raw === "object" && raw !== null && "id" in raw + ? (raw as { id: string }).id.trim() + : typeof raw === "string" + ? raw.trim() + : ""; if (!id || seen.has(id)) continue; seen.add(id); const defaults = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id] ?? {}; @@ -266,9 +277,11 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature; if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call; - // Include context window limit when known — OpenCode reads this to - // determine usable context length for compaction & overflow detection. - const contextLength = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; + // Context window: live model entry (from API catalog) > modelContextLengths > static defaults + const liveContext = typeof raw === "object" && raw !== null + ? (raw as { contextLength?: number }).contextLength + : undefined; + const contextLength = liveContext ?? options.modelContextLengths?.[id] ?? OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; if (typeof contextLength === "number" && contextLength > 0) { entry.limit = { context: contextLength }; } @@ -508,7 +521,7 @@ export interface OmniRouteLiveModel { * const config = buildOmniRouteOpenCodeConfig({ * baseURL: "http://localhost:20128", * apiKey: "sk_omniroute", - * models: models.map((m) => m.id), + * models, // OmniRouteLiveModel[] — contextLength auto-extracted * modelLabels: Object.fromEntries(models.map((m) => [m.id, m.name])), * }); * ``` From dcb5e889bb75b5a5fac6d3ab7034ec29a11c8dca Mon Sep 17 00:00:00 2001 From: Herjarsa Date: Sat, 6 Jun 2026 17:07:17 +0200 Subject: [PATCH 2/4] fix(catalog): compute combo context_length from known targets only Previously, minKnownNumber() required ALL targets to have known context lengths. If any single target was unknown (e.g. passthrough model not in registry), the entire combo defaulted to 128K instead of computing the minimum from the known targets. This change makes minKnownNumber() ignore undefined values and compute the minimum from the subset of known targets. buildComboCatalogMetadata() now also filters out null targets instead of aborting the entire aggregation. Fixes nested combos where some child targets lack registry entries. + regression test for mixed-known/unknown target combos --- src/app/api/v1/models/catalog.ts | 11 +++++---- tests/unit/models-catalog-route.test.ts | 32 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index f5baf366906b..a0fb1334739b 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -99,8 +99,9 @@ function intersectStringArrays(arrays: string[][]): string[] { } function minKnownNumber(values: Array): number | undefined { - if (values.length === 0 || !values.every(isPositiveFiniteNumber)) return undefined; - return Math.min(...values); + const knownValues = values.filter(isPositiveFiniteNumber); + if (knownValues.length === 0) return undefined; + return Math.min(...knownValues); } const VISION_MODEL_KEYWORDS = [ @@ -579,9 +580,11 @@ export async function getUnifiedModelsResponse( if (targets.length === 0) return baseMetadata; const targetMetadata = targets.map((target) => getComboTargetCatalogMetadata(target)); - if (targetMetadata.some((metadata) => metadata === null)) return baseMetadata; - const knownMetadata = targetMetadata as ComboTargetCatalogMetadata[]; + const knownMetadata = targetMetadata.filter( + (metadata): metadata is ComboTargetCatalogMetadata => metadata !== null + ); + if (knownMetadata.length === 0) return baseMetadata; const contextLength = explicitContextLength ?? minKnownNumber(knownMetadata.map((metadata) => metadata.contextLength)); diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index 036277892093..11579942080e 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -1340,6 +1340,38 @@ test("v1 models catalog prefers manual combo context_length over auto-calculated assert.equal(comboModel.context_length, 64000, "manual context_length should override auto-calc"); }); +test("v1 models catalog computes combo context_length from known targets when some targets have unknown context", async () => { + await seedConnection("openai", { name: "openai-mixed-context" }); + await seedConnection("claude", { + authType: "oauth", + name: "claude-mixed-context", + apiKey: null, + accessToken: "claude-access", + }); + + // Create a combo with targets: one known (gpt-4o = 128K), one unknown (nonexistent-model). + // The combo should still compute context_length = 128K from the known target. + const combo = await combosDb.createCombo({ + name: "mixed-context-combo", + strategy: "priority", + models: ["openai/gpt-4o", "openai/nonexistent-model-xyz"], + }); + + 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 === "mixed-context-combo"); + + assert.equal(response.status, 200); + assert.ok(comboModel); + assert.equal( + comboModel.context_length, + 128000, + "combo context_length should be the MIN of known target model limits, ignoring unknown targets" + ); +}); + // Regression test for Issue #2798: noAuth providers (opencode/oc) have no DB connection rows // but their models must still appear in /v1/models. test("v1 models catalog includes noAuth provider models when no DB connections exist (#2798)", async () => { From b7c2b4a72669110d5d66629b0b655f448182debc Mon Sep 17 00:00:00 2001 From: Herjarsa Date: Sat, 6 Jun 2026 12:20:30 -0300 Subject: [PATCH 3/4] test(opencode-provider): cover live-model contextLength + fix indent Adds two regression tests proving contextLength is read from a live model entry (not just the legacy 8-id static map): one for an id absent from the static map, one asserting the live value wins over a static default. Also restores the lost 2-space indent on the modelCapabilities field. Co-authored-by: diegosouzapw --- @omniroute/opencode-provider/src/index.ts | 2 +- .../opencode-provider/tests/index.test.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index 258c52c5afa5..c312e900405f 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -138,7 +138,7 @@ export interface OmniRouteProviderOptions { * `OMNIROUTE_DEFAULT_MODEL_CAPABILITIES` for ids in the default catalog; * for custom ids the override is used verbatim. */ -modelCapabilities?: Record; + modelCapabilities?: Record; /** * Optional per-model context-length overrides (tokens). Takes precedence * over the static `OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS` map but is diff --git a/@omniroute/opencode-provider/tests/index.test.ts b/@omniroute/opencode-provider/tests/index.test.ts index 50d3439a3300..aac6e7a27bee 100644 --- a/@omniroute/opencode-provider/tests/index.test.ts +++ b/@omniroute/opencode-provider/tests/index.test.ts @@ -430,6 +430,34 @@ test("createOmniRouteProvider omits limit.context for unknown model ids", () => assert.equal(entry.limit, undefined); }); +test("createOmniRouteProvider reads contextLength from a live model entry for ids absent from the static map", () => { + // #3298 regression guard: the static OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS + // map only covers the legacy 8 Claude/Gemini ids. Before this change, any + // other model got `undefined` context (see the test above, string form) and + // OpenCode silently fell back to its 128K internal default. A live model + // entry carrying `contextLength` must now surface as `limit.context`. + const provider = createOmniRouteProvider({ + baseURL: "http://localhost:20128", + apiKey: "sk_omniroute", + models: [{ id: "completely-unknown-model", contextLength: 262_144 }], + }); + const entry = provider.models["completely-unknown-model"]; + assert.ok(entry.limit, "a live contextLength should produce a limit field even for ids absent from the static map"); + assert.equal(entry.limit!.context, 262_144); +}); + +test("createOmniRouteProvider: a live model contextLength wins over the static default map", () => { + // `cc/claude-opus-4-8` has a static default (1_000_000). A live entry carrying + // a different contextLength must take precedence (live > modelContextLengths > + // static defaults). + const provider = createOmniRouteProvider({ + baseURL: "http://localhost:20128", + apiKey: "sk_omniroute", + models: [{ id: "cc/claude-opus-4-8", contextLength: 524_288 }], + }); + assert.equal(provider.models["cc/claude-opus-4-8"].limit!.context, 524_288); +}); + test("createOmniRouteProvider serialises limit.context to JSON", () => { const provider = createOmniRouteProvider({ baseURL: "http://localhost:20128", From 712a971b07e6737bdc22928424b65f61b12f3242 Mon Sep 17 00:00:00 2001 From: Herjarsa Date: Sat, 6 Jun 2026 17:41:04 +0200 Subject: [PATCH 4/4] fix(opencode-provider): apply Gemini Code Assist review feedback - Fix potential runtime TypeError when raw.id is not a string - Parse string values from modelContextLengths to numbers - Add NaN guard after parseInt Addresses PR #3304 review comments. --- @omniroute/opencode-provider/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index c312e900405f..976c8e640b7e 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -255,7 +255,7 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open const seen = new Set(); for (const raw of modelList) { const id = - typeof raw === "object" && raw !== null && "id" in raw + typeof raw === "object" && raw !== null && "id" in raw && typeof (raw as any).id === "string" ? (raw as { id: string }).id.trim() : typeof raw === "string" ? raw.trim() @@ -281,8 +281,9 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open const liveContext = typeof raw === "object" && raw !== null ? (raw as { contextLength?: number }).contextLength : undefined; - const contextLength = liveContext ?? options.modelContextLengths?.[id] ?? OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; - if (typeof contextLength === "number" && contextLength > 0) { + const rawContextLength = liveContext ?? options.modelContextLengths?.[id] ?? OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; + const contextLength = typeof rawContextLength === "string" ? parseInt(rawContextLength, 10) : rawContextLength; + if (typeof contextLength === "number" && !isNaN(contextLength) && contextLength > 0) { entry.limit = { context: contextLength }; }