From bf1f1ca0632ed4f99528fe807f7416533fef37f7 Mon Sep 17 00:00:00 2001 From: Benjamin Gonzalez Date: Sun, 29 Mar 2026 23:34:23 -0500 Subject: [PATCH 1/4] Fill missing OpenRouter models by using API when not found in models.dev --- packages/opencode/src/provider/provider.ts | 96 ++++++++++++++++++++-- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 7fb3166284be..d97f740d0c15 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -404,7 +404,7 @@ export namespace Provider { }, } }, - openrouter: async () => { + openrouter: async (input) => { return { autoload: false, options: { @@ -413,6 +413,85 @@ export namespace Provider { "X-Title": "opencode", }, }, + async discoverModels(): Promise> { + const cachePath = path.join(Global.Path.cache, "discovery-openrouter.json") + const stats = Filesystem.stat(cachePath) + const dbStats = Filesystem.stat(path.join(Global.Path.cache, "models.json")) + + const isStale = + !stats || + (dbStats && stats.mtime < dbStats.mtime) || + Date.now() - stats.mtime.getTime() > 1000 * 60 * 60 + + if (!isStale) { + const cached = await Filesystem.readJson>(cachePath).catch(() => undefined) + if (cached) return cached + } + + const res = await fetch("https://openrouter.ai/api/v1/models").then((x) => x.json() as Promise<{ data: any[] }>) + const discovered: Record = {} + for (const m of res.data) { + if (input.models[m.id]) continue + const hasReasoning = + m.supported_parameters?.includes("reasoning") || m.supported_parameters?.includes("include_reasoning") + const hasTools = m.supported_parameters?.includes("tools") + const inputModalities = m.architecture?.input_modalities ?? ["text"] + const outputModalities = m.architecture?.output_modalities ?? ["text"] + const model: Model = { + id: ModelID.make(m.id), + providerID: ProviderID.openrouter, + name: m.name, + family: m.architecture?.tokenizer ?? "", + api: { + id: m.id, + url: "https://openrouter.ai/api/v1", + npm: "@openrouter/ai-sdk-provider", + }, + status: "active", + headers: {}, + options: {}, + cost: { + input: Number(m.pricing?.prompt ?? 0) * 1000000, + output: (Number(m.pricing?.completion ?? 0) + Number(m.pricing?.internal_reasoning ?? 0)) * 1000000, + cache: { + read: Number(m.pricing?.input_cache_read ?? 0) * 1000000, + write: Number(m.pricing?.input_cache_write ?? 0) * 1000000, + }, + }, + limit: { + context: m.context_length, + output: m.top_provider?.max_completion_tokens ?? 0, + }, + capabilities: { + temperature: m.supported_parameters?.includes("temperature") ?? true, + reasoning: hasReasoning, + attachment: inputModalities.includes("image") || inputModalities.includes("file"), + toolcall: hasTools, + input: { + text: true, + audio: inputModalities.includes("audio"), + image: inputModalities.includes("image"), + video: inputModalities.includes("video"), + pdf: inputModalities.includes("file") || inputModalities.includes("pdf"), + }, + output: { + text: outputModalities.includes("text"), + audio: outputModalities.includes("audio"), + image: outputModalities.includes("image"), + video: outputModalities.includes("video"), + pdf: false, + }, + interleaved: false, + }, + release_date: m.created ? new Date(m.created * 1000).toISOString().split("T")[0] : m.knowledge_cutoff ?? "", + variants: {}, + } + model.variants = mapValues(ProviderTransform.variants(model), (v) => v) + discovered[m.id] = model + } + await Filesystem.writeJson(cachePath, discovered).catch(() => undefined) + return discovered + }, } }, vercel: async () => { @@ -1179,16 +1258,19 @@ export namespace Provider { log.info("found", { providerID }) } - const gitlab = ProviderID.make("gitlab") - if (discoveryLoaders[gitlab] && providers[gitlab]) { + for (const [id, discover] of Object.entries(discoveryLoaders)) { + const providerID = ProviderID.make(id) + const provider = providers[providerID] + if (!provider) continue + await (async () => { - const discovered = await discoveryLoaders[gitlab]() + const discovered = await discover() for (const [modelID, model] of Object.entries(discovered)) { - if (!providers[gitlab].models[modelID]) { - providers[gitlab].models[modelID] = model + if (!provider.models[modelID]) { + provider.models[modelID] = model } } - })().catch((e) => log.warn("state discovery error", { id: "gitlab", error: e })) + })().catch((e) => log.warn("state discovery error", { id, error: e })) } return { From 0fdddcc45afdab19874397ec713ba5d273b2b256 Mon Sep 17 00:00:00 2001 From: Benjamin Gonzalez Date: Mon, 30 Mar 2026 00:31:04 -0500 Subject: [PATCH 2/4] Fix OpenRouter cost cannot be negative For auto router the cost is marked as -1 but we can't use that, so just use 0 --- packages/opencode/src/provider/provider.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d97f740d0c15..c2292c3fd9d6 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -451,11 +451,12 @@ export namespace Provider { headers: {}, options: {}, cost: { - input: Number(m.pricing?.prompt ?? 0) * 1000000, - output: (Number(m.pricing?.completion ?? 0) + Number(m.pricing?.internal_reasoning ?? 0)) * 1000000, + input: Math.max(0, Number(m.pricing?.prompt ?? 0)) * 1000000, + output: + Math.max(0, Number(m.pricing?.completion ?? 0) + Number(m.pricing?.internal_reasoning ?? 0)) * 1000000, cache: { - read: Number(m.pricing?.input_cache_read ?? 0) * 1000000, - write: Number(m.pricing?.input_cache_write ?? 0) * 1000000, + read: Math.max(0, Number(m.pricing?.input_cache_read ?? 0)) * 1000000, + write: Math.max(0, Number(m.pricing?.input_cache_write ?? 0)) * 1000000, }, }, limit: { From 1d4ab042fcbbb66c8bc2240acc95db0c78dd7574 Mon Sep 17 00:00:00 2001 From: Benjamin Gonzalez Date: Mon, 30 Mar 2026 00:40:44 -0500 Subject: [PATCH 3/4] Prune models.dev OpenRouter models that do not exist in OpenRouter model list --- packages/opencode/src/provider/provider.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c2292c3fd9d6..abb65380f83b 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -429,9 +429,14 @@ export namespace Provider { } const res = await fetch("https://openrouter.ai/api/v1/models").then((x) => x.json() as Promise<{ data: any[] }>) + if (!res.data?.length) return {} + const discovered: Record = {} for (const m of res.data) { - if (input.models[m.id]) continue + if (input.models[m.id]) { + discovered[m.id] = input.models[m.id] + continue + } const hasReasoning = m.supported_parameters?.includes("reasoning") || m.supported_parameters?.includes("include_reasoning") const hasTools = m.supported_parameters?.includes("tools") @@ -1266,6 +1271,15 @@ export namespace Provider { await (async () => { const discovered = await discover() + + if (providerID === ProviderID.openrouter) { + for (const modelID of Object.keys(provider.models)) { + if (!discovered[modelID]) { + delete provider.models[modelID] + } + } + } + for (const [modelID, model] of Object.entries(discovered)) { if (!provider.models[modelID]) { provider.models[modelID] = model From e9c6f12bc855138ba074c5a8c1d6b3ff12acabe1 Mon Sep 17 00:00:00 2001 From: Benjamin Gonzalez Date: Mon, 30 Mar 2026 02:43:01 -0500 Subject: [PATCH 4/4] Fetched OpenRouter models inherit interleaved property Fixes minimax-m2.5:free --- packages/opencode/src/provider/provider.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index abb65380f83b..59f2bfb34376 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -437,11 +437,20 @@ export namespace Provider { discovered[m.id] = input.models[m.id] continue } + const id = String(m.id ?? "") const hasReasoning = m.supported_parameters?.includes("reasoning") || m.supported_parameters?.includes("include_reasoning") const hasTools = m.supported_parameters?.includes("tools") const inputModalities = m.architecture?.input_modalities ?? ["text"] const outputModalities = m.architecture?.output_modalities ?? ["text"] + const base = id.endsWith(":free") ? id.slice(0, -5) : "" + const inherited = iife(() => { + if (!base) return undefined + const interleaved = input.models[base]?.capabilities.interleaved + if (typeof interleaved !== "object") return undefined + return interleaved + }) + const interleaved = inherited ?? false const model: Model = { id: ModelID.make(m.id), providerID: ProviderID.openrouter, @@ -487,7 +496,7 @@ export namespace Provider { video: outputModalities.includes("video"), pdf: false, }, - interleaved: false, + interleaved, }, release_date: m.created ? new Date(m.created * 1000).toISOString().split("T")[0] : m.knowledge_cutoff ?? "", variants: {},