Explorar el Código

fix(provider): type default model failures (#28881)

Shoubhit Dash hace 3 meses
padre
commit
5f42351159

+ 1 - 1
packages/opencode/src/agent/agent.ts

@@ -69,7 +69,7 @@ export interface Interface {
       whenToUse: string
       systemPrompt: string
     },
-    Provider.ModelNotFoundError
+    Provider.DefaultModelError
   >
 }
 

+ 29 - 3
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 = {

+ 19 - 4
packages/opencode/src/provider/provider.ts

@@ -994,7 +994,22 @@ export class InitError extends Schema.TaggedErrorClass<InitError>()("ProviderIni
   }
 }
 
-export type Error = ModelNotFoundError | InitError
+export class NoProvidersError extends Schema.TaggedErrorClass<NoProvidersError>()("ProviderNoProvidersError", {}) {
+  static isInstance(input: unknown): input is NoProvidersError {
+    return input instanceof NoProvidersError
+  }
+}
+
+export class NoModelsError extends Schema.TaggedErrorClass<NoModelsError>()("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<Record<ProviderID, Info>>
@@ -1006,7 +1021,7 @@ export interface Interface {
     query: string[],
   ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
   readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
-  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,

+ 1 - 1
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) {

+ 10 - 0
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* () {