diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 560781ef0e4..7271ba4a754 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -460,6 +460,13 @@ export const ProvidersLoginCommand = effectCmd({ ) } + if (provider === "google-vertex") { + yield* Prompt.log.info( + "Note: this API key cannot be used with Anthropic models.\n" + + "Use `gcloud auth application-default login` or set `GOOGLE_APPLICATION_CREDENTIALS`.", + ) + } + if (provider === "opencode") { yield* Prompt.log.info("Create an api key at https://opencode.ai/auth") } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 874afa05b4b..0a8beaccf0c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -384,6 +384,15 @@ function ApiMethod(props: ApiMethodProps) { ), + "google-vertex": ( + + Note: this API key cannot be used with Anthropic models. + + Use gcloud auth application-default login or set + GOOGLE_APPLICATION_CREDENTIALS. + + + ), }[props.providerID] ?? undefined } onConfirm={async (value) => { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 29cc83a1eb8..bd590cf8c4e 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1095,8 +1095,9 @@ export function latest(msgs: WithParts[]) { export function fromError( e: unknown, - ctx: { providerID: ProviderID; aborted?: boolean }, + ctx: { model: Provider.Model; aborted?: boolean } | { providerID: ProviderID; aborted?: boolean }, ): NonNullable { + const providerID = "model" in ctx ? ctx.model.providerID : ctx.providerID switch (true) { case e instanceof DOMException && e.name === "AbortError": return new AbortedError( @@ -1110,11 +1111,23 @@ export function fromError( case LoadAPIKeyError.isInstance(e): return new AuthError( { - providerID: ctx.providerID, + providerID, message: e.message, }, { cause: e }, ).toObject() + case e instanceof Error && + "model" in ctx && + ctx.model.api.npm === "@ai-sdk/google-vertex/anthropic" && + e.message.includes("Could not load the default credentials"): + return new AuthError( + { + providerID, + message: + "Anthropic models on Google Vertex require Google Cloud credentials. Use `gcloud auth application-default login` or set `GOOGLE_APPLICATION_CREDENTIALS`.", + }, + { cause: e }, + ).toObject() case (e as SystemError)?.code === "ECONNRESET": return new APIError( { @@ -1157,7 +1170,7 @@ export function fromError( ).toObject() case APICallError.isInstance(e): const parsed = ProviderError.parseAPICallError({ - providerID: ctx.providerID, + providerID, error: e, }) if (parsed.type === "context_overflow") { diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index a287c3b0068..b8d3025f336 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -124,7 +124,7 @@ export const layer = Layer.effect( const parse = (e: unknown) => MessageV2.fromError(e, { - providerID: input.model.providerID, + model: input.model, aborted, }) diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 82bed0e9cc6..7eb74ebe155 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1509,6 +1509,37 @@ describe("session.message-v2.fromError", () => { }) }) + test("explains ADC requirements when Vertex Anthropic credentials are missing", () => { + const vertexAnthropicModel = { + ...model, + providerID: ProviderID.googleVertex, + api: { ...model.api, npm: "@ai-sdk/google-vertex/anthropic" }, + } + const result = MessageV2.fromError( + new Error( + "Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.", + ), + { model: vertexAnthropicModel }, + ) + + expect(result).toStrictEqual({ + name: "ProviderAuthError", + data: { + providerID: "google-vertex", + message: + "Anthropic models on Google Vertex require Google Cloud credentials. Use `gcloud auth application-default login` or set `GOOGLE_APPLICATION_CREDENTIALS`.", + }, + }) + }) + + test("does not present Anthropic ADC guidance for Gemini Vertex credentials errors", () => { + const result = MessageV2.fromError(new Error("Could not load the default credentials."), { + model: { ...model, providerID: ProviderID.googleVertex, api: { ...model.api, npm: "@ai-sdk/google-vertex" } }, + }) + + expect(result.name).toBe("UnknownError") + }) + test("serializes tagged errors with their message", () => { const result = MessageV2.fromError(new Question.RejectedError(), { providerID })