diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index e78304e4e7..f7e2c134a3 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -69,7 +69,7 @@ export interface Interface { whenToUse: string systemPrompt: string }, - Provider.ModelNotFoundError + Provider.DefaultModelError > } diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index ac9879ff89..1cd8b1dee3 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -1,6 +1,6 @@ import { EOL } from "os" import { basename } from "path" -import { Effect } from "effect" +import { Cause, Effect } from "effect" import { Agent } from "../../../agent/agent" import { Provider } from "@/provider/provider" import { Session } from "@/session/session" @@ -80,7 +80,21 @@ const run = Effect.fn("Cli.debug.agent.body")(function* ( const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) { const provider = yield* Provider.Service const registry = yield* ToolRegistry.Service - const model = agent.model ?? (yield* provider.defaultModel()) + const model = + agent.model ?? + (yield* provider.defaultModel().pipe( + Effect.matchCauseEffect({ + onSuccess: Effect.succeed, + onFailure: (cause) => { + const error = Cause.squash(cause) as Provider.DefaultModelError + if (error instanceof Provider.ModelNotFoundError) { + return fail(`Model not found: ${error.providerID}/${error.modelID}`) + } + if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`) + return fail("No providers found") + }, + }), + )) return yield* registry.tools({ ...model, agent }) }) @@ -133,7 +147,19 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio ? agent.model : yield* Effect.gen(function* () { const provider = yield* Provider.Service - return yield* provider.defaultModel() + return yield* provider.defaultModel().pipe( + Effect.matchCauseEffect({ + onSuccess: Effect.succeed, + onFailure: (cause) => { + const error = Cause.squash(cause) as Provider.DefaultModelError + if (error instanceof Provider.ModelNotFoundError) { + return fail(`Model not found: ${error.providerID}/${error.modelID}`) + } + if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`) + return fail("No providers found") + }, + }), + ) }) const now = Date.now() const message: MessageV2.Assistant = { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2fe2b7a4a5..2140bea4df 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -994,7 +994,22 @@ export class InitError extends Schema.TaggedErrorClass()("ProviderIni } } -export type Error = ModelNotFoundError | InitError +export class NoProvidersError extends Schema.TaggedErrorClass()("ProviderNoProvidersError", {}) { + static isInstance(input: unknown): input is NoProvidersError { + return input instanceof NoProvidersError + } +} + +export class NoModelsError extends Schema.TaggedErrorClass()("ProviderNoModelsError", { + providerID: ProviderID, +}) { + static isInstance(input: unknown): input is NoModelsError { + return input instanceof NoModelsError + } +} + +export type DefaultModelError = ModelNotFoundError | NoProvidersError | NoModelsError +export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModelsError export interface Interface { readonly list: () => Effect.Effect> @@ -1006,7 +1021,7 @@ export interface Interface { query: string[], ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> readonly getSmallModel: (providerID: ProviderID) => Effect.Effect - readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }> + readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }, DefaultModelError> } interface State { @@ -1821,9 +1836,9 @@ export const layer = Layer.effect( } const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)) - if (!provider) throw new Error("no providers found") + if (!provider) return yield* new NoProvidersError() const [model] = sort(Object.values(provider.models)) - if (!model) throw new Error("no models found") + if (!model) return yield* new NoModelsError({ providerID: provider.id }) return { providerID: provider.id, modelID: model.id, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fc9fa0b96a..2fc93c4825 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -682,7 +682,7 @@ export const layer = Layer.effect( .findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model) .pipe(Effect.orDie) if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model - return yield* provider.defaultModel() + return yield* provider.defaultModel().pipe(Effect.orDie) }) const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8e3276dbab..8cf93e22d6 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -351,6 +351,16 @@ it.instance( { config: { model: "anthropic/claude-sonnet-4-20250514" } }, ) +it.instance( + "defaultModel returns a typed error when config excludes every provider", + Effect.gen(function* () { + const error = yield* Provider.use.defaultModel().pipe(Effect.flip) + expect(error).toBeInstanceOf(Provider.NoProvidersError) + expect(error._tag).toBe("ProviderNoProvidersError") + }), + { config: { enabled_providers: [] } }, +) + it.instance( "provider with baseURL from config", Effect.gen(function* () {