fix(provider): restore model suggestions (#27372)
This commit is contained in:
@@ -979,6 +979,7 @@ export interface Interface {
|
|||||||
interface State {
|
interface State {
|
||||||
models: Map<string, LanguageModelV3>
|
models: Map<string, LanguageModelV3>
|
||||||
providers: Record<ProviderID, Info>
|
providers: Record<ProviderID, Info>
|
||||||
|
catalog: Record<ProviderID, Info>
|
||||||
sdk: Map<string, BundledSDK>
|
sdk: Map<string, BundledSDK>
|
||||||
modelLoaders: Record<string, CustomModelLoader>
|
modelLoaders: Record<string, CustomModelLoader>
|
||||||
varsLoaders: Record<string, CustomVarsLoader>
|
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(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -1120,7 +1153,8 @@ const layer = Layer.effect(
|
|||||||
const bridge = yield* EffectBridge.make()
|
const bridge = yield* EffectBridge.make()
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
const modelsDev = yield* modelsDevSvc.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 providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
|
||||||
const languages = new Map<string, LanguageModelV3>()
|
const languages = new Map<string, LanguageModelV3>()
|
||||||
@@ -1437,6 +1471,7 @@ const layer = Layer.effect(
|
|||||||
return {
|
return {
|
||||||
models: languages,
|
models: languages,
|
||||||
providers,
|
providers,
|
||||||
|
catalog,
|
||||||
sdk,
|
sdk,
|
||||||
modelLoaders,
|
modelLoaders,
|
||||||
varsLoaders,
|
varsLoaders,
|
||||||
@@ -1597,16 +1632,20 @@ const layer = Layer.effect(
|
|||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
const provider = s.providers[providerID]
|
const provider = s.providers[providerID]
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
const available = Object.keys(s.providers)
|
const catalogProvider = s.catalog[providerID]
|
||||||
const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
|
const suggestions = catalogProvider
|
||||||
throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
|
? 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]
|
const info = provider.models[modelID]
|
||||||
if (!info) {
|
if (!info) {
|
||||||
const available = Object.keys(provider.models)
|
const current = modelSuggestions(provider, modelID)
|
||||||
const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
|
const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID)
|
||||||
throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { testEffect } from "../lib/effect"
|
|||||||
|
|
||||||
const env = makeRuntime(Env.Service, Env.defaultLayer)
|
const env = makeRuntime(Env.Service, Env.defaultLayer)
|
||||||
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
|
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>) {
|
async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
|
||||||
return AppRuntime.runPromise(
|
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 () => {
|
test("getProvider returns undefined for nonexistent provider", async () => {
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
init: async (dir) => {
|
init: async (dir) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user