1+ import * as vscode from "vscode"
12import { Anthropic } from "@anthropic-ai/sdk"
23import OpenAI from "openai"
34
@@ -9,8 +10,9 @@ import {
910} from "@roo-code/types"
1011
1112import { ApiHandlerOptions } from "../../shared/api"
12- import { getCachedZooCodeToken , getZooCodeBaseUrl } from "../../services/zoo-code-auth"
13+ import { clearZooCodeToken , getCachedZooCodeToken , getZooCodeBaseUrl } from "../../services/zoo-code-auth"
1314import { Package } from "../../shared/package"
15+ import { t } from "../../i18n"
1416
1517import { ApiStream } from "../transform/stream"
1618import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -19,6 +21,74 @@ import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway"
1921import type { SingleCompletionHandler , ApiHandlerCreateMessageMetadata } from "../index"
2022import { 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)
2393interface 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 }
0 commit comments