Procházet zdrojové kódy

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

Shoubhit Dash před 3 měsíci
rodič
revize
20cec91550

+ 46 - 7
packages/opencode/src/provider/provider.ts

@@ -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
     })

+ 27 - 0
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<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) => {