From 20cec9155021c37fe557eb17559a35dc0c2b692c Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 14 May 2026 00:38:38 +0530 Subject: [PATCH] fix(provider): restore model suggestions (#27372) --- packages/opencode/src/provider/provider.ts | 53 ++++++++++++++++--- .../opencode/test/provider/provider.test.ts | 27 ++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ca87c40b7d..0f757caf19 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -979,6 +979,7 @@ export interface Interface { interface State { models: Map providers: Record + catalog: Record sdk: Map modelLoaders: Record varsLoaders: Record @@ -1104,6 +1105,38 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { } } +function suggestionModelIDs(provider: Info | undefined) { + if (!provider) return [] + return Object.keys(provider.models).filter((id) => { + const model = provider.models[id] + if (model.status === "deprecated") return false + if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) return false + return true + }) +} + +function modelSuggestions(provider: Info | undefined, modelID: ModelID) { + const available = suggestionModelIDs(provider) + const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target) + if (fuzzy.length) return fuzzy + const query = modelID + .toLowerCase() + .split(/[^a-z0-9]+/) + .filter((part) => part.length > 1) + return sortBy( + available + .map((id) => ({ + id, + score: query.filter((part) => id.toLowerCase().includes(part)).length, + })) + .filter((item) => item.score > 0), + [(item) => item.score, "desc"], + [(item) => item.id, "asc"], + ) + .slice(0, 3) + .map((item) => item.id) +} + const layer = Layer.effect( Service, Effect.gen(function* () { @@ -1120,7 +1153,8 @@ const layer = Layer.effect( const bridge = yield* EffectBridge.make() const cfg = yield* config.get() const modelsDev = yield* modelsDevSvc.get() - const database = mapValues(modelsDev, fromModelsDevProvider) + const catalog = mapValues(modelsDev, fromModelsDevProvider) + const database = mapValues(catalog, toPublicInfo) const providers: Record = {} as Record const languages = new Map() @@ -1437,6 +1471,7 @@ const layer = Layer.effect( return { models: languages, providers, + catalog, sdk, modelLoaders, varsLoaders, @@ -1597,16 +1632,20 @@ const layer = Layer.effect( const s = yield* InstanceState.get(state) const provider = s.providers[providerID] if (!provider) { - const available = Object.keys(s.providers) - const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 }) - throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) + const catalogProvider = s.catalog[providerID] + const suggestions = catalogProvider + ? modelSuggestions(catalogProvider, modelID) + : fuzzysort + .go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 }) + .map((m) => m.target) + throw new ModelNotFoundError({ providerID, modelID, suggestions }) } const info = provider.models[modelID] if (!info) { - const available = Object.keys(provider.models) - const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }) - throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) + const current = modelSuggestions(provider, modelID) + const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID) + throw new ModelNotFoundError({ providerID, modelID, suggestions }) } return info }) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 2270418beb..ab39c19025 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -19,6 +19,7 @@ import { testEffect } from "../lib/effect" const env = makeRuntime(Env.Service, Env.defaultLayer) const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v)) +const remove = (k: string) => env.runSync((svc) => svc.remove(k)) async function run(fn: (provider: Provider.Interface) => Effect.Effect) { return AppRuntime.runPromise( @@ -1604,6 +1605,32 @@ test("ModelNotFoundError for provider includes suggestions", async () => { }) }) +test("ModelNotFoundError suggests catalog models for unloaded providers", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await WithInstance.provide({ + directory: tmp.path, + fn: async () => { + remove("OPENCODE_API_KEY") + try { + await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model")) + throw new Error("expected model lookup to fail") + } catch (e) { + if (!Provider.ModelNotFoundError.isInstance(e)) throw e + expect(e.data.suggestions).toContain("claude-haiku-4-5") + } + }, + }) +}) + test("getProvider returns undefined for nonexistent provider", async () => { await using tmp = await tmpdir({ init: async (dir) => {