fix(provider): type model not found errors (#27334)

This commit is contained in:
Kit Langton
2026-05-13 18:23:09 -04:00
committed by GitHub
parent 5182a3698d
commit 55e0af1405
12 changed files with 340 additions and 69 deletions

View File

@@ -21,6 +21,7 @@ import { pathToFileURL } from "url"
import { Effect, Layer, Context, Schema, Types } from "effect"
import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state"
import { EffectPromise } from "@/effect/promise"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { isRecord } from "@/util/record"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
@@ -963,11 +964,24 @@ export function defaultModelIDs<T extends { models: Record<string, { id: string
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
}
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
providerID: ProviderID,
modelID: ModelID,
suggestions: Schema.optional(Schema.Array(Schema.String)),
cause: Schema.optional(Schema.Defect),
}) {
static isInstance(input: unknown): input is ModelNotFoundError {
return input instanceof ModelNotFoundError
}
}
export type Error = ModelNotFoundError
export interface Interface {
readonly list: () => Effect.Effect<Record<ProviderID, Info>>
readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model>
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model, ModelNotFoundError>
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
readonly closest: (
providerID: ProviderID,
query: string[],
@@ -1638,14 +1652,14 @@ const layer = Layer.effect(
: fuzzysort
.go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
.map((m) => m.target)
throw new ModelNotFoundError({ providerID, modelID, suggestions })
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
}
const info = provider.models[modelID]
if (!info) {
const current = modelSuggestions(provider, modelID)
const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID)
throw new ModelNotFoundError({ providerID, modelID, suggestions })
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
}
return info
})
@@ -1656,11 +1670,10 @@ const layer = Layer.effect(
const key = `${model.providerID}/${model.id}`
if (s.models.has(key)) return s.models.get(key)!
return yield* Effect.promise(async () => {
const provider = s.providers[model.providerID]
const sdk = await resolveSDK(model, s, envs)
try {
const provider = s.providers[model.providerID]
return yield* EffectPromise.refineRejection(
async () => {
const sdk = await resolveSDK(model, s, envs)
const language = s.modelLoaders[model.providerID]
? await s.modelLoaders[model.providerID](sdk, model.api.id, {
...provider.options,
@@ -1669,18 +1682,12 @@ const layer = Layer.effect(
: sdk.languageModel(model.api.id)
s.models.set(key, language)
return language
} catch (e) {
if (e instanceof NoSuchModelError)
throw new ModelNotFoundError(
{
modelID: model.id,
providerID: model.providerID,
},
{ cause: e },
)
throw e
}
})
},
(cause) =>
cause instanceof NoSuchModelError
? new ModelNotFoundError({ modelID: model.id, providerID: model.providerID, cause })
: undefined,
)
})
const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
@@ -1700,7 +1707,7 @@ const layer = Layer.effect(
if (cfg.small_model) {
const parsed = parseModel(cfg.small_model)
return yield* getModel(parsed.providerID, parsed.modelID)
return yield* getModel(parsed.providerID, parsed.modelID).pipe(Effect.orDie)
}
const s = yield* InstanceState.get(state)
@@ -1728,22 +1735,22 @@ const layer = Layer.effect(
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
const globalMatch = candidates.find((m) => m.startsWith("global."))
if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch)).pipe(Effect.orDie)
const region = provider.options?.region
if (region) {
const regionPrefix = region.split("-")[0]
if (regionPrefix === "us" || regionPrefix === "eu") {
const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch)).pipe(Effect.orDie)
}
}
const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed)).pipe(Effect.orDie)
} else {
for (const model of Object.keys(provider.models)) {
if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model)).pipe(Effect.orDie)
}
}
}
@@ -1818,12 +1825,6 @@ export function parseModel(model: string) {
}
}
export const ModelNotFoundError = NamedError.create("ProviderModelNotFoundError", {
providerID: ProviderID,
modelID: ModelID,
suggestions: Schema.optional(Schema.Array(Schema.String)),
})
export const InitError = NamedError.create("ProviderInitError", {
providerID: ProviderID,
})