fix(provider): type model not found errors (#27334)
This commit is contained in:
@@ -62,11 +62,14 @@ export interface Interface {
|
||||
readonly generate: (input: {
|
||||
description: string
|
||||
model?: { providerID: ProviderID; modelID: ModelID }
|
||||
}) => Effect.Effect<{
|
||||
identifier: string
|
||||
whenToUse: string
|
||||
systemPrompt: string
|
||||
}>
|
||||
}) => Effect.Effect<
|
||||
{
|
||||
identifier: string
|
||||
whenToUse: string
|
||||
systemPrompt: string
|
||||
},
|
||||
Provider.ModelNotFoundError
|
||||
>
|
||||
}
|
||||
|
||||
type State = Omit<Interface, "generate">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { errorFormat } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
interface ErrorLike {
|
||||
name?: string
|
||||
@@ -10,10 +11,6 @@ interface ErrorLike {
|
||||
|
||||
type ConfigIssue = { message: string; path: string[] }
|
||||
|
||||
function isRecord(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null
|
||||
}
|
||||
|
||||
function isTaggedError(error: unknown, tag: string): boolean {
|
||||
return isRecord(error) && error._tag === tag
|
||||
}
|
||||
@@ -61,11 +58,13 @@ export function FormatError(input: unknown) {
|
||||
}
|
||||
|
||||
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
|
||||
if (NamedError.hasName(input, "ProviderModelNotFoundError")) {
|
||||
const data = (input as ErrorLike).data
|
||||
const suggestions = Array.isArray(data?.suggestions) ? data.suggestions.filter((x) => typeof x === "string") : []
|
||||
const providerModelNotFound = configData(input, "ProviderModelNotFoundError")
|
||||
if (providerModelNotFound) {
|
||||
const suggestions = Array.isArray(providerModelNotFound.suggestions)
|
||||
? providerModelNotFound.suggestions.filter((x) => typeof x === "string")
|
||||
: []
|
||||
return [
|
||||
`Model not found: ${data?.providerID}/${data?.modelID}`,
|
||||
`Model not found: ${providerModelNotFound.providerID}/${providerModelNotFound.modelID}`,
|
||||
...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
|
||||
`Try: \`opencode models\` to list available models`,
|
||||
`Or check your config (opencode.json) provider/model names`,
|
||||
|
||||
17
packages/opencode/src/effect/promise.ts
Normal file
17
packages/opencode/src/effect/promise.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Cause, Effect } from "effect"
|
||||
|
||||
export function refineRejection<A, E>(
|
||||
evaluate: (signal: AbortSignal) => PromiseLike<A>,
|
||||
refine: (cause: unknown) => E | undefined,
|
||||
) {
|
||||
return Effect.tryPromise(evaluate).pipe(
|
||||
Effect.catch((error) => {
|
||||
const cause = Cause.isUnknownError(error) ? error.cause : error
|
||||
const refined = refine(cause)
|
||||
if (refined !== undefined) return Effect.fail(refined)
|
||||
return Effect.die(cause)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export * as EffectPromise from "./promise"
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { iife } from "@/util/iife"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Cause, Effect } from "effect"
|
||||
@@ -23,14 +21,7 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
|
||||
log.error("failed", { error, cause: Cause.pretty(cause) })
|
||||
|
||||
if (error instanceof NamedError) {
|
||||
return Effect.succeed(
|
||||
HttpServerResponse.jsonUnsafe(error.toObject(), {
|
||||
status: iife(() => {
|
||||
if (error instanceof Provider.ModelNotFoundError) return 400
|
||||
return 500
|
||||
}),
|
||||
}),
|
||||
)
|
||||
return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 500 }))
|
||||
}
|
||||
return Effect.succeed(
|
||||
HttpServerResponse.jsonUnsafe(
|
||||
|
||||
@@ -390,8 +390,8 @@ export const layer: Layer.Layer<
|
||||
|
||||
const agent = yield* agents.get("compaction")
|
||||
const model = agent.model
|
||||
? yield* provider.getModel(agent.model.providerID, agent.model.modelID)
|
||||
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
|
||||
? yield* provider.getModel(agent.model.providerID, agent.model.modelID).pipe(Effect.orDie)
|
||||
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID).pipe(Effect.orDie)
|
||||
const cfg = yield* config.get()
|
||||
const history = compactionPart && messages.at(-1)?.info.id === input.parentID ? messages.slice(0, -1) : messages
|
||||
const prior = completedCompactions(history)
|
||||
@@ -519,7 +519,9 @@ export const layer: Layer.Layer<
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
agent: userMessage.agent,
|
||||
model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID),
|
||||
model: yield* provider
|
||||
.getModel(userMessage.model.providerID, userMessage.model.modelID)
|
||||
.pipe(Effect.orDie),
|
||||
provider: {
|
||||
source: info.source,
|
||||
info,
|
||||
|
||||
@@ -1057,15 +1057,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
if (Exit.isSuccess(exit)) return exit.value
|
||||
const err = Cause.squash(exit.cause)
|
||||
if (Provider.ModelNotFoundError.isInstance(err)) {
|
||||
const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : ""
|
||||
const hint = err.suggestions?.length ? ` Did you mean: ${err.suggestions.join(", ")}?` : ""
|
||||
yield* bus.publish(Session.Event.Error, {
|
||||
sessionID,
|
||||
error: new NamedError.Unknown({
|
||||
message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`,
|
||||
message: `Model not found: ${err.providerID}/${err.modelID}.${hint}`,
|
||||
}).toObject(),
|
||||
})
|
||||
}
|
||||
return yield* Effect.failCause(exit.cause)
|
||||
return yield* Effect.die(err)
|
||||
})
|
||||
|
||||
const currentModel = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
@@ -1108,7 +1108,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
|
||||
const full =
|
||||
!input.variant && ag.variant && same
|
||||
? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void))
|
||||
? yield* provider
|
||||
.getModel(model.providerID, model.modelID)
|
||||
.pipe(Effect.catchIf(Provider.ModelNotFoundError.isInstance, () => Effect.succeed(undefined)))
|
||||
: undefined
|
||||
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user