Skip to content

Commit d4050f8

Browse files
James Mtendamemacursoragent
andcommitted
fix(zoo-gateway): surface 401/402/403 errors with actionable toasts
Clear the cached token on 401 and offer sign-in. On insufficient credits or budget limits, open the credits page. On account frozen/banned, open support. Errors still propagate to the task layer after the toast. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 49c9133 commit d4050f8

2 files changed

Lines changed: 116 additions & 31 deletions

File tree

src/api/providers/zoo-gateway.ts

Lines changed: 106 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as vscode from "vscode"
12
import { Anthropic } from "@anthropic-ai/sdk"
23
import OpenAI from "openai"
34

@@ -9,8 +10,9 @@ import {
910
} from "@roo-code/types"
1011

1112
import { ApiHandlerOptions } from "../../shared/api"
12-
import { getCachedZooCodeToken, getZooCodeBaseUrl } from "../../services/zoo-code-auth"
13+
import { clearZooCodeToken, getCachedZooCodeToken, getZooCodeBaseUrl } from "../../services/zoo-code-auth"
1314
import { Package } from "../../shared/package"
15+
import { t } from "../../i18n"
1416

1517
import { ApiStream } from "../transform/stream"
1618
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -19,6 +21,74 @@ import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway"
1921
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
2022
import { RouterProvider } from "./router-provider"
2123

24+
function getApiErrorStatus(error: unknown): number | undefined {
25+
if (typeof error === "object" && error !== null && "status" in error) {
26+
const status = (error as { status: unknown }).status
27+
if (typeof status === "number") return status
28+
}
29+
return undefined
30+
}
31+
32+
function getApiErrorCode(error: unknown): string | undefined {
33+
const err = error as { code?: unknown; error?: { code?: unknown } } | null
34+
if (!err) return undefined
35+
if (typeof err.code === "string") return err.code
36+
if (typeof err.error?.code === "string") return err.error.code
37+
return undefined
38+
}
39+
40+
function buildZooCodeSignInUrl(): string {
41+
const callbackUri = encodeURIComponent(
42+
`${vscode.env.uriScheme}://${Package.publisher}.${Package.name}/auth-callback`,
43+
)
44+
const device = encodeURIComponent(vscode.env.appName || "VS Code")
45+
const editor = encodeURIComponent("VS Code")
46+
return `${getZooCodeBaseUrl()}/dashboard/connect?device=${device}&editor=${editor}&version=${Package.version}&callback_uri=${callbackUri}`
47+
}
48+
49+
// Caller must always rethrow — this only surfaces UX, never swallows.
50+
async function surfaceGatewayApiError(error: unknown): Promise<void> {
51+
const status = getApiErrorStatus(error)
52+
if (status === undefined) return
53+
const code = getApiErrorCode(error)
54+
55+
if (status === 401) {
56+
// Wipe before sign-in so the callback rebinds against an empty slot.
57+
await clearZooCodeToken()
58+
const action = await vscode.window.showErrorMessage(
59+
t("common:zooAuth.errors.session_expired"),
60+
t("common:zooAuth.buttons.sign_in"),
61+
)
62+
if (action) {
63+
void vscode.env.openExternal(vscode.Uri.parse(buildZooCodeSignInUrl()))
64+
}
65+
return
66+
}
67+
68+
const isBudgetExceeded = status === 429 && (code === "monthly_budget_exceeded" || code === "daily_budget_exceeded")
69+
if (status === 402 || isBudgetExceeded) {
70+
const message = isBudgetExceeded
71+
? t("common:zooAuth.errors.budget_exceeded")
72+
: t("common:zooAuth.errors.out_of_credits")
73+
const action = await vscode.window.showErrorMessage(message, t("common:zooAuth.buttons.add_credits"))
74+
if (action) {
75+
void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/dashboard/credits`))
76+
}
77+
return
78+
}
79+
80+
if (status === 403) {
81+
const action = await vscode.window.showErrorMessage(
82+
t("common:zooAuth.errors.account_unavailable"),
83+
t("common:zooAuth.buttons.contact_support"),
84+
)
85+
if (action) {
86+
void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/support`))
87+
}
88+
return
89+
}
90+
}
91+
2292
// Extend OpenAI's CompletionUsage to include Zoo Gateway specific fields (same as Vercel AI Gateway)
2393
interface ZooGatewayUsage extends OpenAI.CompletionUsage {
2494
cache_creation_input_tokens?: number
@@ -100,43 +170,48 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio
100170
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
101171
}
102172

103-
const completion = await this.client.chat.completions.create(body, {
104-
headers: requestHeaders,
105-
})
173+
try {
174+
const completion = await this.client.chat.completions.create(body, {
175+
headers: requestHeaders,
176+
})
106177

107-
for await (const chunk of completion) {
108-
const delta = chunk.choices[0]?.delta
109-
if (delta?.content) {
110-
yield {
111-
type: "text",
112-
text: delta.content,
178+
for await (const chunk of completion) {
179+
const delta = chunk.choices[0]?.delta
180+
if (delta?.content) {
181+
yield {
182+
type: "text",
183+
text: delta.content,
184+
}
113185
}
114-
}
115186

116-
// Emit raw tool call chunks - NativeToolCallParser handles state management
117-
if (delta?.tool_calls) {
118-
for (const toolCall of delta.tool_calls) {
119-
yield {
120-
type: "tool_call_partial",
121-
index: toolCall.index,
122-
id: toolCall.id,
123-
name: toolCall.function?.name,
124-
arguments: toolCall.function?.arguments,
187+
// Emit raw tool call chunks - NativeToolCallParser handles state management
188+
if (delta?.tool_calls) {
189+
for (const toolCall of delta.tool_calls) {
190+
yield {
191+
type: "tool_call_partial",
192+
index: toolCall.index,
193+
id: toolCall.id,
194+
name: toolCall.function?.name,
195+
arguments: toolCall.function?.arguments,
196+
}
125197
}
126198
}
127-
}
128199

129-
if (chunk.usage) {
130-
const usage = chunk.usage as ZooGatewayUsage
131-
yield {
132-
type: "usage",
133-
inputTokens: usage.prompt_tokens || 0,
134-
outputTokens: usage.completion_tokens || 0,
135-
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
136-
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
137-
totalCost: usage.cost ?? 0,
200+
if (chunk.usage) {
201+
const usage = chunk.usage as ZooGatewayUsage
202+
yield {
203+
type: "usage",
204+
inputTokens: usage.prompt_tokens || 0,
205+
outputTokens: usage.completion_tokens || 0,
206+
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
207+
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
208+
totalCost: usage.cost ?? 0,
209+
}
138210
}
139211
}
212+
} catch (error) {
213+
void surfaceGatewayApiError(error)
214+
throw error
140215
}
141216
}
142217

@@ -159,6 +234,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio
159234
const response = await this.client.chat.completions.create(requestOptions)
160235
return response.choices[0]?.message.content || ""
161236
} catch (error) {
237+
void surfaceGatewayApiError(error)
162238
if (error instanceof Error) {
163239
throw new Error(`Zoo Gateway completion error: ${error.message}`)
164240
}

src/i18n/locales/en/common.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,20 @@
253253
"invalid_token_received": "Zoo Code: Invalid authentication token received.",
254254
"token_verification_failed": "Zoo Code: Token verification failed.",
255255
"invalid_token": "Zoo Code: Invalid token.",
256-
"could_not_verify_token": "Zoo Code: Could not verify token."
256+
"could_not_verify_token": "Zoo Code: Could not verify token.",
257+
"session_expired": "Zoo Code session expired. Please sign in again to continue using Zoo Gateway.",
258+
"out_of_credits": "Zoo Gateway: insufficient credits. Add credits to continue.",
259+
"account_unavailable": "Zoo Gateway: your account is currently unavailable. Please contact support.",
260+
"budget_exceeded": "Zoo Gateway: usage budget reached. Wait for the budget to reset or top up."
257261
},
258262
"info": {
259263
"connected": "Zoo Code: Successfully connected! You can now use Zoo Code as your AI provider.",
260264
"disconnected": "Zoo Code: Disconnected successfully."
265+
},
266+
"buttons": {
267+
"sign_in": "Sign In",
268+
"add_credits": "Add Credits",
269+
"contact_support": "Contact Support"
261270
}
262271
},
263272
"codeActions": {

0 commit comments

Comments
 (0)