fix(provider): restore model suggestions (#27372)

This commit is contained in:
Shoubhit Dash 2026-05-14 00:38:38 +05:30 committed by GitHub
parent 22a5e6cc50
commit 20cec91550
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 7 deletions

View file

@ -979,6 +979,7 @@ export interface Interface {
interface State {
models: Map<string, LanguageModelV3>
providers: Record<ProviderID, Info>
catalog: Record<ProviderID, Info>
sdk: Map<string, BundledSDK>
modelLoaders: Record<string, CustomModelLoader>
varsLoaders: Record<string, CustomVarsLoader>
@ -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<ProviderID, Info> = {} as Record<ProviderID, Info>
const languages = new Map<string, LanguageModelV3>()
@ -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
})

View file

@ -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<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
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) => {