From 641b2c3c391aad58caa175f12352e113b1d08287 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 07:29:43 -0600 Subject: [PATCH 01/20] feat(zoo-gateway): add provider types, handler, and model fetcher Co-authored-by: Cursor --- packages/types/src/provider-settings.ts | 21 ++- packages/types/src/providers/index.ts | 4 + packages/types/src/providers/zoo-gateway.ts | 24 +++ src/api/index.ts | 3 + src/api/providers/fetchers/modelCache.ts | 40 +++-- src/api/providers/fetchers/zoo-gateway.ts | 123 ++++++++++++++ src/api/providers/index.ts | 1 + src/api/providers/zoo-gateway.ts | 179 ++++++++++++++++++++ src/shared/api.ts | 1 + 9 files changed, 383 insertions(+), 13 deletions(-) create mode 100644 packages/types/src/providers/zoo-gateway.ts create mode 100644 src/api/providers/fetchers/zoo-gateway.ts create mode 100644 src/api/providers/zoo-gateway.ts diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index ef532a9791..700745f41b 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -38,6 +38,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 export const dynamicProviders = [ "openrouter", "vercel-ai-gateway", + "zoo-gateway", "litellm", "requesty", "unbound", @@ -399,6 +400,12 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({ vercelAiGatewayModelId: z.string().optional(), }) +const zooGatewaySchema = baseProviderSettingsSchema.extend({ + zooSessionToken: z.string().optional(), + zooGatewayModelId: z.string().optional(), + zooGatewayBaseUrl: z.string().optional(), +}) + const basetenSchema = apiModelIdProviderModelSchema.extend({ basetenApiKey: z.string().optional(), }) @@ -437,6 +444,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), + zooGatewaySchema.merge(z.object({ apiProvider: z.literal("zoo-gateway") })), defaultSchema, ]) @@ -471,6 +479,7 @@ export const providerSettingsSchema = z.object({ ...fireworksSchema.shape, ...qwenCodeSchema.shape, ...vercelAiGatewaySchema.shape, + ...zooGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, }) @@ -501,6 +510,7 @@ export const modelIdKeys = [ "unboundModelId", "litellmModelId", "vercelAiGatewayModelId", + "zooGatewayModelId", ] as const satisfies readonly (keyof ProviderSettings)[] export type ModelIdKey = (typeof modelIdKeys)[number] @@ -546,6 +556,7 @@ export const modelIdKeysByProvider: Record = { zai: "apiModelId", fireworks: "apiModelId", "vercel-ai-gateway": "vercelAiGatewayModelId", + "zoo-gateway": "zooGatewayModelId", } /** @@ -564,8 +575,13 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str return "anthropic" } - // Vercel AI Gateway uses anthropic protocol for anthropic models. - if (provider && provider === "vercel-ai-gateway" && modelId && modelId.toLowerCase().startsWith("anthropic/")) { + // Vercel AI Gateway, Zoo Gateway, and Roo use anthropic protocol for anthropic models. + if ( + provider && + ["vercel-ai-gateway", "zoo-gateway", "roo"].includes(provider) && + modelId && + modelId.toLowerCase().startsWith("anthropic/") + ) { return "anthropic" } @@ -662,6 +678,7 @@ export const MODELS_BY_PROVIDER: Record< requesty: { id: "requesty", label: "Requesty", models: [] }, unbound: { id: "unbound", label: "Unbound", models: [] }, "vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] }, + "zoo-gateway": { id: "zoo-gateway", label: "Zoo Gateway", models: [] }, // Local providers; models discovered from localhost endpoints. lmstudio: { id: "lmstudio", label: "LM Studio", models: [] }, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index e75f5c4240..04788dfba9 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -25,6 +25,7 @@ export * from "./vercel-ai-gateway.js" export * from "./zai.js" export * from "./minimax.js" export * from "./mimo.js" +export * from "./zoo-gateway.js" import { anthropicDefaultModelId } from "./anthropic.js" import { basetenDefaultModelId } from "./baseten.js" @@ -49,6 +50,7 @@ import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js" import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js" import { minimaxDefaultModelId } from "./minimax.js" import { mimoDefaultModelId } from "./mimo.js" +import { zooGatewayDefaultModelId } from "./zoo-gateway.js" // Import the ProviderName type from provider-settings to avoid duplication import type { ProviderName } from "../provider-settings.js" @@ -115,6 +117,8 @@ export function getProviderDefaultModelId( return unboundDefaultModelId case "vercel-ai-gateway": return vercelAiGatewayDefaultModelId + case "zoo-gateway": + return zooGatewayDefaultModelId case "anthropic": case "gemini-cli": case "fake-ai": diff --git a/packages/types/src/providers/zoo-gateway.ts b/packages/types/src/providers/zoo-gateway.ts new file mode 100644 index 0000000000..8596026441 --- /dev/null +++ b/packages/types/src/providers/zoo-gateway.ts @@ -0,0 +1,24 @@ +import type { ModelInfo } from "../model.js" + +// Zoo Gateway uses the same model ID format as Vercel AI Gateway (provider/model-name) +export const zooGatewayDefaultModelId = "anthropic/claude-sonnet-4" + +// Zoo Gateway serves the same models as Vercel AI Gateway, so prompt caching support is identical +// We reuse VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS from vercel-ai-gateway.ts +// Instead of duplicating, we just export a reference to indicate they're the same +export { VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS as ZOO_GATEWAY_PROMPT_CACHING_MODELS } from "./vercel-ai-gateway.js" + +export const zooGatewayDefaultModelInfo: ModelInfo = { + maxTokens: 64000, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + description: + "Claude Sonnet 4 significantly improves on Sonnet 3.7's industry-leading capabilities, excelling in coding with a state-of-the-art 72.7% on SWE-bench. The model balances performance and efficiency for internal and external use cases, with enhanced steerability for greater control over implementations.", +} + +export const ZOO_GATEWAY_DEFAULT_TEMPERATURE = 0.7 diff --git a/src/api/index.ts b/src/api/index.ts index c9e5e7b1b9..b50ac2492c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -32,6 +32,7 @@ import { ZAiHandler, FireworksHandler, VercelAiGatewayHandler, + ZooGatewayHandler, MiniMaxHandler, MimoHandler, BasetenHandler, @@ -176,6 +177,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new FireworksHandler(options) case "vercel-ai-gateway": return new VercelAiGatewayHandler(options) + case "zoo-gateway": + return new ZooGatewayHandler(options) case "minimax": return new MiniMaxHandler(options) case "baseten": diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index ee4ea5bfe6..e2cad3c46a 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -26,6 +26,7 @@ import { getOllamaModels } from "./ollama" import { getLMStudioModels } from "./lmstudio" import { getPoeModels } from "./poe" import { getDeepSeekModels } from "./deepseek" +import { getZooGatewayModels } from "./zoo-gateway" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -92,6 +93,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise => { const { provider } = options - let models = getModelsFromCache(provider) + // Always fetch fresh to prevent serving stale models from different auth contexts. + const shouldSkipCache = provider === "zoo-gateway" + + let models = shouldSkipCache ? undefined : getModelsFromCache(provider) if (models) { return models @@ -128,13 +135,14 @@ export const getModels = async (options: GetModelsOptions): Promise // Only cache non-empty results to prevent persisting failed API responses // Empty results could indicate API failure rather than "no models exist" - if (modelCount > 0) { + // Zoo Gateway models are user-specific - skip caching entirely + if (modelCount > 0 && !shouldSkipCache) { memoryCache.set(provider, models) await writeModels(provider, models).catch((err) => console.error(`[MODEL_CACHE] Error writing ${provider} models to file cache:`, err), ) - } else { + } else if (modelCount === 0) { TelemetryService.instance.captureEvent(TelemetryEventName.MODEL_CACHE_EMPTY_RESPONSE, { provider, context: "getModels", @@ -163,6 +171,11 @@ export const getModels = async (options: GetModelsOptions): Promise export const refreshModels = async (options: GetModelsOptions): Promise => { const { provider } = options + // Zoo Gateway models are user-specific (auth-scoped). Mirror the bypass in + // getModels() so we never persist one user's model list and serve it to a + // different authenticated user from cache. + const shouldSkipCache = provider === "zoo-gateway" + // Check if there's already an in-flight refresh for this provider // This prevents race conditions where multiple concurrent refreshes might // overwrite each other's results @@ -179,7 +192,7 @@ export const refreshModels = async (options: GetModelsOptions): Promise - console.error(`[refreshModels] Error writing ${provider} models to disk:`, err), - ) + await writeModels(provider, models).catch((err) => + console.error(`[refreshModels] Error writing ${provider} models to disk:`, err), + ) + } return models } catch (error) { - // Log the error for debugging, then return existing cache if available (graceful degradation) + // Log the error for debugging, then return existing cache if available (graceful degradation). + // For auth-scoped providers (zoo-gateway) we MUST NOT return cached models from a prior + // session, since they could belong to a different user — return empty instead. console.error(`[refreshModels] Failed to refresh ${provider} models:`, error) + if (shouldSkipCache) { + return {} + } return getModelsFromCache(provider) || {} } finally { // Always clean up the in-flight tracking diff --git a/src/api/providers/fetchers/zoo-gateway.ts b/src/api/providers/fetchers/zoo-gateway.ts new file mode 100644 index 0000000000..569bbba995 --- /dev/null +++ b/src/api/providers/fetchers/zoo-gateway.ts @@ -0,0 +1,123 @@ +import axios from "axios" + +import type { ModelInfo } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../../shared/api" +import { getZooCodeBaseUrl } from "../../../services/zoo-code-auth" + +// Reuse the same schemas and parsing logic from vercel-ai-gateway since the API format is identical +import { type VercelAiGatewayModel, parseVercelAiGatewayModel } from "./vercel-ai-gateway" + +import { z } from "zod" + +/** + * ZooGatewayPricing (same format as Vercel AI Gateway) + */ + +const zooGatewayPricingSchema = z.object({ + input: z.string().optional(), + output: z.string().optional(), + input_cache_write: z.string().optional(), + input_cache_read: z.string().optional(), + image: z.string().optional(), +}) + +/** + * ZooGatewayModel (same format as Vercel AI Gateway) + */ + +const zooGatewayModelSchema = z.object({ + id: z.string(), + object: z.string(), + created: z.number(), + owned_by: z.string(), + name: z.string(), + description: z.string(), + context_window: z.number(), + max_tokens: z.number(), + type: z.string(), + pricing: zooGatewayPricingSchema, +}) + +/** + * ZooGatewayModelsResponse + */ + +const zooGatewayModelsResponseSchema = z.object({ + object: z.string(), + data: z.array(zooGatewayModelSchema), +}) + +type ZooGatewayModelsResponse = z.infer + +// Bound model discovery so a network stall can't hang provider initialization paths. +const MODEL_DISCOVERY_TIMEOUT_MS = 15_000 + +/** + * getZooGatewayModels + * + * Fetches models from the Zoo Gateway API. Requires authentication via the zoo_ext_ token. + */ + +export async function getZooGatewayModels(options?: ApiHandlerOptions): Promise> { + const models: Record = {} + const baseURL = options?.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` + + // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token + const headers: Record = {} + if (options?.zooSessionToken) { + headers["Authorization"] = `Bearer ${options.zooSessionToken}` + } + + try { + const response = await axios.get(`${baseURL}/models`, { + headers, + timeout: MODEL_DISCOVERY_TIMEOUT_MS, + }) + const result = zooGatewayModelsResponseSchema.safeParse(response.data) + const data = result.success ? result.data.data : response.data.data + + if (!result.success) { + console.error(`Zoo Gateway models response is invalid ${JSON.stringify(result.error.format())}`) + } + + for (const model of data) { + const { id } = model + + // Only include language models for chat inference. + // Embedding models are statically defined in embeddingModels.ts. + if (model.type !== "language") { + continue + } + + // Parse model using the same logic as Vercel AI Gateway since formats are identical + models[id] = parseZooGatewayModel({ id, model: model as VercelAiGatewayModel }) + } + } catch (error) { + // Log only safe fields; never serialize the full error object because it + // includes request config/headers which carry the bearer session token. + const err = error as { + message?: string + name?: string + code?: string + response?: { status?: number; statusText?: string } + } + console.error( + `Error fetching Zoo Gateway models: name=${err.name ?? "Error"} code=${err.code ?? "unknown"} status=${err.response?.status ?? "unknown"} ${err.response?.statusText ?? ""} message=${err.message ?? "unknown error"}`, + ) + } + + return models +} + +/** + * parseZooGatewayModel + * + * Parses a Zoo Gateway model into ModelInfo format. + * Zoo Gateway returns the same format as Vercel AI Gateway, so we can reuse the parsing logic. + */ + +export const parseZooGatewayModel = ({ id, model }: { id: string; model: VercelAiGatewayModel }): ModelInfo => { + // Reuse the parsing logic from vercel-ai-gateway + return parseVercelAiGatewayModel({ id, model }) +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 416cef1c47..98a235948a 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -25,6 +25,7 @@ export { XAIHandler } from "./xai" export { ZAiHandler } from "./zai" export { FireworksHandler } from "./fireworks" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" +export { ZooGatewayHandler } from "./zoo-gateway" export { MiniMaxHandler } from "./minimax" export { MimoHandler } from "./mimo" export { BasetenHandler } from "./baseten" diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts new file mode 100644 index 0000000000..dc425ba508 --- /dev/null +++ b/src/api/providers/zoo-gateway.ts @@ -0,0 +1,179 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + zooGatewayDefaultModelId, + zooGatewayDefaultModelInfo, + ZOO_GATEWAY_DEFAULT_TEMPERATURE, + VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS, +} from "@roo-code/types" + +import { ApiHandlerOptions } from "../../shared/api" +import { getZooCodeBaseUrl } from "../../services/zoo-code-auth" + +import { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway" + +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { RouterProvider } from "./router-provider" + +import { DEFAULT_HEADERS } from "./constants" + +// Extend OpenAI's CompletionUsage to include Zoo Gateway specific fields (same as Vercel AI Gateway) +interface ZooGatewayUsage extends OpenAI.CompletionUsage { + cache_creation_input_tokens?: number + cost?: number +} + +export class ZooGatewayHandler extends RouterProvider implements SingleCompletionHandler { + constructor(options: ApiHandlerOptions) { + const baseURL = options.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` + + // Fail fast with a clear message instead of waiting for a 401. + // The token is set automatically by handleZooCodeCallback() after the user + // authenticates via the "Sign in with Zoo Code" flow in the extension. + if (!options.zooSessionToken) { + throw new Error("Zoo Gateway requires authentication. Please sign in to Zoo Code first.") + } + + super({ + options, + name: "zoo-gateway", + baseURL, + apiKey: options.zooSessionToken, + modelId: options.zooGatewayModelId, + defaultModelId: zooGatewayDefaultModelId, + defaultModelInfo: zooGatewayDefaultModelInfo, + }) + + // Override the client to add Zoo-specific enrichment headers + // These headers help with request tracking and analytics + const enrichmentHeaders: Record = {} + + // Note: These headers will be populated per-request in createMessage + // For now we just set static headers that are always available + if (typeof process !== "undefined" && process.env?.npm_package_version) { + enrichmentHeaders["X-Zoo-Extension-Version"] = process.env.npm_package_version + } + enrichmentHeaders["X-Zoo-Editor"] = "vscode" + + // Recreate client with enrichment headers + ;(this as any).client = new OpenAI({ + baseURL, + apiKey: options.zooSessionToken, + defaultHeaders: { + ...DEFAULT_HEADERS, + ...enrichmentHeaders, + ...(options.openAiHeaders || {}), + }, + }) + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId, info } = await this.fetchModel() + + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + // Apply prompt caching for models that support it + // Zoo Gateway serves the same models as Vercel AI Gateway, so caching support is identical + if (VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS.has(modelId) && info.supportsPromptCache) { + addCacheBreakpoints(systemPrompt, openAiMessages) + } + + // Build request headers with enrichment metadata + const requestHeaders: Record = {} + if (metadata?.taskId) { + requestHeaders["X-Zoo-Task-ID"] = metadata.taskId + } + if (metadata?.mode) { + requestHeaders["X-Zoo-Mode"] = metadata.mode + } + + const body: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: openAiMessages, + temperature: this.supportsTemperature(modelId) + ? (this.options.modelTemperature ?? ZOO_GATEWAY_DEFAULT_TEMPERATURE) + : undefined, + max_completion_tokens: info.maxTokens, + stream: true, + stream_options: { include_usage: true }, + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? true, + } + + const completion = await this.client.chat.completions.create(body, { + headers: requestHeaders, + }) + + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + // Emit raw tool call chunks - NativeToolCallParser handles state management + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } + } + } + + if (chunk.usage) { + const usage = chunk.usage as ZooGatewayUsage + yield { + type: "usage", + inputTokens: usage.prompt_tokens || 0, + outputTokens: usage.completion_tokens || 0, + cacheWriteTokens: usage.cache_creation_input_tokens || undefined, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined, + totalCost: usage.cost ?? 0, + } + } + } + } + + async completePrompt(prompt: string): Promise { + const { id: modelId, info } = await this.fetchModel() + + try { + const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + stream: false, + } + + if (this.supportsTemperature(modelId)) { + requestOptions.temperature = this.options.modelTemperature ?? ZOO_GATEWAY_DEFAULT_TEMPERATURE + } + + requestOptions.max_completion_tokens = info.maxTokens + + const response = await this.client.chat.completions.create(requestOptions) + return response.choices[0]?.message.content || "" + } catch (error) { + if (error instanceof Error) { + throw new Error(`Zoo Gateway completion error: ${error.message}`) + } + throw error + } + } +} diff --git a/src/shared/api.ts b/src/shared/api.ts index a6f31855ca..678156a183 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -171,6 +171,7 @@ type CommonFetchParams = { const dynamicProviderExtras = { openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type "vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + "zoo-gateway": {} as { apiKey?: string; baseUrl?: string }, litellm: {} as { apiKey: string; baseUrl: string }, requesty: {} as { apiKey?: string; baseUrl?: string }, unbound: {} as { apiKey?: string }, From 9ae1c350671959609c3ab5c25e7bfec742420b45 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 07:54:27 -0600 Subject: [PATCH 02/20] fix(zoo-gateway): respect readonly client, real version header, safer fetch - Stop reassigning RouterProvider.client; thread Zoo enrichment headers through openAiHeaders so a single OpenAI client is used. - Replace npm_package_version (never populated at extension runtime) with Package.version from the shared package shim. - Default the model list to [] on a structurally broken response so we log and recover instead of crashing on response.data.data being undefined. - Bypass inFlightRefresh de-duplication for zoo-gateway: a refresh triggered after sign-out/sign-in must not return the previous user's in-flight response. - Add fetcher unit tests covering auth header, timeout, error redaction, and bad-response handling. Co-authored-by: Cursor --- .../fetchers/__tests__/zoo-gateway.spec.ts | 150 ++++++++++++++++++ src/api/providers/fetchers/modelCache.ts | 25 ++- src/api/providers/fetchers/zoo-gateway.ts | 5 +- src/api/providers/zoo-gateway.ts | 38 ++--- 4 files changed, 184 insertions(+), 34 deletions(-) create mode 100644 src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts diff --git a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts new file mode 100644 index 0000000000..8fc2690216 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts @@ -0,0 +1,150 @@ +// npx vitest run src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts + +import axios from "axios" + +import { getZooGatewayModels, parseZooGatewayModel } from "../zoo-gateway" + +vitest.mock("axios") +const mockedAxios = axios as any + +describe("Zoo Gateway Fetchers", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + describe("getZooGatewayModels", () => { + const baseUrl = "https://example.test/api/gateway/v1" + const token = "zoo_ext_test_token" + + const mockResponse = { + data: { + object: "list", + data: [ + { + id: "anthropic/claude-sonnet-4", + object: "model", + created: 1640995200, + owned_by: "anthropic", + name: "Claude Sonnet 4", + description: "Sonnet 4", + context_window: 200000, + max_tokens: 64000, + type: "language", + pricing: { + input: "3.00", + output: "15.00", + input_cache_write: "3.75", + input_cache_read: "0.30", + }, + }, + { + id: "image/dall-e-3", + object: "model", + created: 1640995200, + owned_by: "openai", + name: "DALL-E 3", + description: "Image", + context_window: 4000, + max_tokens: 1000, + type: "image", + pricing: { input: "40.00", output: "0.00" }, + }, + ], + }, + } + + it("forwards the bearer token and timeout, filters non-language models", async () => { + mockedAxios.get.mockResolvedValueOnce(mockResponse) + + const models = await getZooGatewayModels({ + zooGatewayBaseUrl: baseUrl, + zooSessionToken: token, + } as any) + + expect(mockedAxios.get).toHaveBeenCalledWith( + `${baseUrl}/models`, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: `Bearer ${token}` }), + timeout: expect.any(Number), + }), + ) + expect(Object.keys(models)).toHaveLength(1) + expect(models["anthropic/claude-sonnet-4"]).toBeDefined() + }) + + it("omits the Authorization header when no token is provided", async () => { + mockedAxios.get.mockResolvedValueOnce(mockResponse) + + await getZooGatewayModels({ zooGatewayBaseUrl: baseUrl } as any) + + const call = mockedAxios.get.mock.calls[0] + expect(call[1].headers.Authorization).toBeUndefined() + }) + + it("returns {} and never leaks the error object when the request fails", async () => { + const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) + const failure: any = new Error("Network error") + // Simulate axios attaching the request config (which contains the bearer token). + failure.config = { headers: { Authorization: "Bearer should-never-be-logged" } } + failure.code = "ECONNRESET" + failure.response = { status: 502, statusText: "Bad Gateway" } + mockedAxios.get.mockRejectedValueOnce(failure) + + const models = await getZooGatewayModels({ + zooGatewayBaseUrl: baseUrl, + zooSessionToken: token, + } as any) + + expect(models).toEqual({}) + const logged = consoleErrorSpy.mock.calls.map((args) => String(args[0])).join("\n") + expect(logged).toContain("status=502") + expect(logged).toContain("code=ECONNRESET") + expect(logged).not.toContain("should-never-be-logged") + expect(logged).not.toContain("Authorization") + consoleErrorSpy.mockRestore() + }) + + it("returns {} on a structurally broken response instead of throwing", async () => { + const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) + mockedAxios.get.mockResolvedValueOnce({ data: { unexpected: true } }) + + const models = await getZooGatewayModels({ + zooGatewayBaseUrl: baseUrl, + zooSessionToken: token, + } as any) + + expect(models).toEqual({}) + expect(consoleErrorSpy).toHaveBeenCalled() + consoleErrorSpy.mockRestore() + }) + }) + + describe("parseZooGatewayModel", () => { + it("delegates to the vercel-ai-gateway parser", () => { + const result = parseZooGatewayModel({ + id: "anthropic/claude-sonnet-4", + model: { + id: "anthropic/claude-sonnet-4", + object: "model", + created: 0, + owned_by: "anthropic", + name: "Claude Sonnet 4", + description: "Sonnet", + context_window: 200000, + max_tokens: 64000, + type: "language", + pricing: { + input: "3.00", + output: "15.00", + input_cache_write: "3.75", + input_cache_read: "0.30", + }, + } as any, + }) + + expect(result.contextWindow).toBe(200000) + expect(result.maxTokens).toBe(64000) + expect(result.supportsPromptCache).toBe(true) + }) + }) +}) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index e2cad3c46a..5936150d68 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -176,12 +176,17 @@ export const refreshModels = async (options: GetModelsOptions): Promise = {} - - // Note: These headers will be populated per-request in createMessage - // For now we just set static headers that are always available - if (typeof process !== "undefined" && process.env?.npm_package_version) { - enrichmentHeaders["X-Zoo-Extension-Version"] = process.env.npm_package_version - } - enrichmentHeaders["X-Zoo-Editor"] = "vscode" - - // Recreate client with enrichment headers - ;(this as any).client = new OpenAI({ - baseURL, - apiKey: options.zooSessionToken, - defaultHeaders: { - ...DEFAULT_HEADERS, - ...enrichmentHeaders, - ...(options.openAiHeaders || {}), - }, - }) } override async *createMessage( From 7cdd52e711780982cff592830eae79c5d0c0987d Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 08:10:08 -0600 Subject: [PATCH 03/20] fix(types): include zoo-gateway key in requestRouterModels record so RouterName stays exhaustive Co-authored-by: Cursor --- src/core/webview/webviewMessageHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 429de051b8..10141c1c87 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -922,6 +922,7 @@ export const webviewMessageHandler = async ( : { openrouter: {}, "vercel-ai-gateway": {}, + "zoo-gateway": {}, litellm: {}, requesty: {}, unbound: {}, From dfb1eed00d6ecc2aeea68c2852443a75ecf46548 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 08:23:18 -0600 Subject: [PATCH 04/20] fix(webview-ui): handle zoo-gateway in useSelectedModel switch so RouterName exhaustiveness holds Co-authored-by: Cursor --- webview-ui/src/components/ui/hooks/useSelectedModel.ts | 9 +++++++++ webview-ui/src/utils/__tests__/validate.spec.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index c4f3040084..bab13b7e9e 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -346,6 +346,15 @@ function getSelectedModel({ const info = routerModels["vercel-ai-gateway"]?.[id] return { id, info } } + case "zoo-gateway": { + const id = getValidatedModelId( + apiConfiguration.zooGatewayModelId, + routerModels["zoo-gateway"], + defaultModelId, + ) + const info = routerModels["zoo-gateway"]?.[id] + return { id, info } + } // case "anthropic": // case "fake-ai": default: { diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 7d6152a03d..0416151f8e 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -44,6 +44,7 @@ describe("Model Validation Functions", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": {}, + "zoo-gateway": {}, poe: {}, deepseek: {}, } From 4df54a0de726629cc471046dfb1b8cbe6a5d6b9c Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 08:40:45 -0600 Subject: [PATCH 05/20] test(router-models): expect zoo-gateway in requestRouterModels responses Co-authored-by: Cursor --- src/core/webview/__tests__/ClineProvider.spec.ts | 3 +++ src/core/webview/__tests__/webviewMessageHandler.spec.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index be9d705684..3a4858edad 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2477,6 +2477,7 @@ describe("ClineProvider - Router Models", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -2523,6 +2524,7 @@ describe("ClineProvider - Router Models", () => { lmstudio: {}, litellm: {}, "vercel-ai-gateway": mockModels, + "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -2618,6 +2620,7 @@ describe("ClineProvider - Router Models", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + "zoo-gateway": {}, poe: {}, deepseek: {}, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 17e0caebb0..32e0f9b48c 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -369,6 +369,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -455,6 +456,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -510,6 +512,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + "zoo-gateway": {}, poe: {}, deepseek: {}, }, From df5be1d738d0bc6eeaba179cafc00e220ad2bbf1 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 09:34:15 -0600 Subject: [PATCH 06/20] test(zoo-gateway): add ZooGatewayHandler unit tests for codecov patch Cover constructor auth guard, base URL resolution, streaming, task/mode headers, temperature, cache breakpoints, tool calls, and completePrompt. Co-authored-by: Cursor --- .../providers/__tests__/zoo-gateway.spec.ts | 324 ++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 src/api/providers/__tests__/zoo-gateway.spec.ts diff --git a/src/api/providers/__tests__/zoo-gateway.spec.ts b/src/api/providers/__tests__/zoo-gateway.spec.ts new file mode 100644 index 0000000000..cba0c68ce5 --- /dev/null +++ b/src/api/providers/__tests__/zoo-gateway.spec.ts @@ -0,0 +1,324 @@ +// npx vitest run src/api/providers/__tests__/zoo-gateway.spec.ts + +vitest.mock("vscode", () => ({})) + +import OpenAI from "openai" + +import { zooGatewayDefaultModelId, ZOO_GATEWAY_DEFAULT_TEMPERATURE } from "@roo-code/types" + +import { ZooGatewayHandler } from "../zoo-gateway" +import { ApiHandlerOptions } from "../../../shared/api" +import { Package } from "../../../shared/package" + +vitest.mock("openai") +vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) })) +vitest.mock("../fetchers/modelCache", () => ({ + getModels: vitest.fn().mockImplementation(() => { + return Promise.resolve({ + "anthropic/claude-sonnet-4": { + maxTokens: 64000, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + description: "Claude Sonnet 4", + }, + "anthropic/claude-3.5-haiku": { + maxTokens: 32000, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 1, + outputPrice: 5, + cacheWritesPrice: 1.25, + cacheReadsPrice: 0.1, + description: "Claude 3.5 Haiku", + }, + }) + }), + getModelsFromCache: vitest.fn().mockReturnValue(undefined), +})) + +vitest.mock("../../../services/zoo-code-auth", () => ({ + getZooCodeBaseUrl: vitest.fn(() => "https://www.zoocode.dev"), +})) + +vitest.mock("../../transform/caching/vercel-ai-gateway", () => ({ + addCacheBreakpoints: vitest.fn(), +})) + +const mockCreate = vitest.fn() + +function mockOpenAIClient() { + vitest.mocked(OpenAI).mockImplementation( + () => + ({ + chat: { + completions: { + create: mockCreate, + }, + }, + }) as unknown as OpenAI, + ) +} + +mockOpenAIClient() + +describe("ZooGatewayHandler", () => { + const mockOptions: ApiHandlerOptions = { + zooSessionToken: "zoo_ext_test_token", + zooGatewayModelId: "anthropic/claude-sonnet-4", + } + + beforeEach(() => { + vitest.clearAllMocks() + mockCreate.mockClear() + mockOpenAIClient() + }) + + describe("constructor", () => { + it("requires authentication before constructing the client", () => { + expect(() => new ZooGatewayHandler({})).toThrow( + "Zoo Gateway requires authentication. Please sign in to Zoo Code first.", + ) + expect(OpenAI).not.toHaveBeenCalled() + }) + + it("initializes OpenAI with Zoo enrichment headers and session token", () => { + const handler = new ZooGatewayHandler({ + ...mockOptions, + zooGatewayBaseUrl: "https://staging.zoocode.dev/api/gateway/v1", + }) + + expect(handler).toBeInstanceOf(ZooGatewayHandler) + expect(OpenAI).toHaveBeenCalledWith({ + baseURL: "https://staging.zoocode.dev/api/gateway/v1", + apiKey: mockOptions.zooSessionToken, + defaultHeaders: expect.objectContaining({ + "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", + "X-Title": "Roo Code", + "X-Zoo-Editor": "vscode", + "X-Zoo-Extension-Version": Package.version, + }), + }) + }) + + it("defaults the gateway base URL from getZooCodeBaseUrl", () => { + new ZooGatewayHandler(mockOptions) + + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://www.zoocode.dev/api/gateway/v1", + }), + ) + }) + }) + + describe("fetchModel", () => { + it("returns configured model info", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const result = await handler.fetchModel() + + expect(result.id).toBe(mockOptions.zooGatewayModelId) + expect(result.info.maxTokens).toBe(64000) + expect(result.info.supportsPromptCache).toBe(true) + }) + + it("falls back to the default model when none is configured", async () => { + const handler = new ZooGatewayHandler({ zooSessionToken: "zoo_ext_test_token" }) + const result = await handler.fetchModel() + + expect(result.id).toBe(zooGatewayDefaultModelId) + }) + }) + + describe("createMessage", () => { + beforeEach(() => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Test response" }, index: 0 }], + usage: null, + } + yield { + choices: [{ delta: {}, index: 0 }], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + cache_creation_input_tokens: 2, + prompt_tokens_details: { cached_tokens: 3 }, + cost: 0.005, + }, + } + }, + })) + }) + + it("streams text and usage chunks", async () => { + const handler = new ZooGatewayHandler(mockOptions) + const stream = handler.createMessage("You are helpful.", [{ role: "user", content: "Hello" }]) + + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "text", text: "Test response" }, + { + type: "usage", + inputTokens: 10, + outputTokens: 5, + cacheWriteTokens: 2, + cacheReadTokens: 3, + totalCost: 0.005, + }, + ]) + }) + + it("forwards task and mode metadata as request headers", async () => { + const handler = new ZooGatewayHandler(mockOptions) + + await handler.createMessage("prompt", [], { taskId: "task-123", mode: "code" }).next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + headers: { + "X-Zoo-Task-ID": "task-123", + "X-Zoo-Mode": "code", + }, + }), + ) + }) + + it("uses custom temperature when provided", async () => { + const handler = new ZooGatewayHandler({ + ...mockOptions, + modelTemperature: 0.5, + }) + + await handler.createMessage("prompt", [{ role: "user", content: "Hi" }]).next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + temperature: 0.5, + }), + expect.any(Object), + ) + }) + + it("uses the default temperature when none is provided", async () => { + const handler = new ZooGatewayHandler(mockOptions) + + await handler.createMessage("prompt", [{ role: "user", content: "Hi" }]).next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + temperature: ZOO_GATEWAY_DEFAULT_TEMPERATURE, + }), + expect.any(Object), + ) + }) + + it("adds cache breakpoints for supported models", async () => { + const { addCacheBreakpoints } = await import("../../transform/caching/vercel-ai-gateway") + const handler = new ZooGatewayHandler({ + ...mockOptions, + zooGatewayModelId: "anthropic/claude-3.5-haiku", + }) + + await handler.createMessage("prompt", [{ role: "user", content: "Hi" }]).next() + + expect(addCacheBreakpoints).toHaveBeenCalled() + }) + + it("yields tool_call_partial chunks when streaming tool calls", async () => { + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_123", + function: { name: "test_tool", arguments: '{"arg1":' }, + }, + ], + }, + index: 0, + }, + ], + } + }, + })) + + const handler = new ZooGatewayHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("prompt", [])) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { + type: "tool_call_partial", + index: 0, + id: "call_123", + name: "test_tool", + arguments: '{"arg1":', + }, + ]) + }) + }) + + describe("completePrompt", () => { + beforeEach(() => { + mockCreate.mockImplementation(async () => ({ + choices: [{ message: { role: "assistant", content: "Test completion response" } }], + })) + }) + + it("returns completion text from the gateway", async () => { + const handler = new ZooGatewayHandler(mockOptions) + + const result = await handler.completePrompt("Complete this: Hello") + + expect(result).toBe("Test completion response") + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "anthropic/claude-sonnet-4", + messages: [{ role: "user", content: "Complete this: Hello" }], + stream: false, + temperature: ZOO_GATEWAY_DEFAULT_TEMPERATURE, + max_completion_tokens: 64000, + }), + ) + }) + + it("wraps errors with a Zoo Gateway prefix", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw new Error("upstream failure") + }) + + await expect(handler.completePrompt("Test")).rejects.toThrow( + "Zoo Gateway completion error: upstream failure", + ) + }) + + it("returns an empty string when the model returns no content", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(async () => ({ + choices: [{ message: { role: "assistant", content: null } }], + })) + + await expect(handler.completePrompt("Test")).resolves.toBe("") + }) + }) +}) From 6f2b1893de9a4e7507f772aa4f4e7f43b6f1f2f5 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 28 May 2026 15:17:41 -0600 Subject: [PATCH 07/20] test(zoo-gateway): mock cached-token + clear-token auth helpers The downstream stack (settings-ui) calls getCachedZooCodeToken and clearZooCodeToken from the auth handler. CI on stacked PRs merges base into head so this spec runs against the cached-token-aware handler; expand the auth module mock so the auth guard test exercises the real throw path instead of vitest's missing-mock-export error. Co-authored-by: Cursor --- src/api/providers/__tests__/zoo-gateway.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/providers/__tests__/zoo-gateway.spec.ts b/src/api/providers/__tests__/zoo-gateway.spec.ts index cba0c68ce5..bfc02d79aa 100644 --- a/src/api/providers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/__tests__/zoo-gateway.spec.ts @@ -44,6 +44,8 @@ vitest.mock("../fetchers/modelCache", () => ({ vitest.mock("../../../services/zoo-code-auth", () => ({ getZooCodeBaseUrl: vitest.fn(() => "https://www.zoocode.dev"), + getCachedZooCodeToken: vitest.fn(() => undefined), + clearZooCodeToken: vitest.fn(async () => undefined), })) vitest.mock("../../transform/caching/vercel-ai-gateway", () => ({ From 21f88048d1d035be9acd744146ac3e0273c65e97 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 07:32:57 -0600 Subject: [PATCH 08/20] feat(zoo-gateway): add settings UI, validation, and i18n Co-authored-by: Cursor --- webview-ui/package.json | 7 +- .../src/components/settings/ApiOptions.tsx | 26 +++++- .../src/components/settings/ModelPicker.tsx | 1 + .../settings/__tests__/ApiOptions.spec.tsx | 24 +++++- .../src/components/settings/constants.ts | 1 + .../settings/providers/ZooGateway.tsx | 83 +++++++++++++++++++ .../components/settings/providers/index.ts | 1 + .../welcome/WelcomeViewProvider.tsx | 14 +++- webview-ui/src/i18n/locales/ca/settings.json | 13 ++- webview-ui/src/i18n/locales/de/settings.json | 13 ++- webview-ui/src/i18n/locales/en/settings.json | 13 ++- webview-ui/src/i18n/locales/es/settings.json | 13 ++- webview-ui/src/i18n/locales/fr/settings.json | 13 ++- webview-ui/src/i18n/locales/hi/settings.json | 13 ++- webview-ui/src/i18n/locales/id/settings.json | 13 ++- webview-ui/src/i18n/locales/it/settings.json | 13 ++- webview-ui/src/i18n/locales/ja/settings.json | 13 ++- webview-ui/src/i18n/locales/ko/settings.json | 13 ++- webview-ui/src/i18n/locales/nl/settings.json | 13 ++- webview-ui/src/i18n/locales/pl/settings.json | 13 ++- .../src/i18n/locales/pt-BR/settings.json | 13 ++- webview-ui/src/i18n/locales/ru/settings.json | 13 ++- webview-ui/src/i18n/locales/tr/settings.json | 13 ++- webview-ui/src/i18n/locales/vi/settings.json | 13 ++- .../src/i18n/locales/zh-CN/settings.json | 13 ++- .../src/i18n/locales/zh-TW/settings.json | 13 ++- webview-ui/src/utils/validate.ts | 16 +++- webview-ui/vite.config.ts | 67 ++++++++++++--- 28 files changed, 397 insertions(+), 77 deletions(-) create mode 100644 webview-ui/src/components/settings/providers/ZooGateway.tsx diff --git a/webview-ui/package.json b/webview-ui/package.json index 047ac73ad2..7d4baa0204 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -48,6 +48,7 @@ "fzf": "^0.5.2", "hast-util-to-jsx-runtime": "^2.3.6", "i18next": "^25.0.0", + "i18next-http-backend": "^3.0.2", "katex": "^0.16.11", "knuth-shuffle-seeded": "^1.0.6", "lru-cache": "^11.1.0", @@ -93,19 +94,19 @@ "@types/diff": "^5.2.1", "@types/jest": "^29.0.0", "@types/katex": "^0.16.7", - "@types/node": "^20.19.25", + "@types/node": "20.x", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.5", "@types/shell-quote": "^1.7.5", "@types/stacktrace-js": "^2.0.3", "@types/vscode-webview": "^1.57.5", - "@vitejs/plugin-react": "^5.2.0", + "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.2.3", "@vitest/ui": "^3.2.3", "babel-plugin-react-compiler": "^1.0.0", "identity-obj-proxy": "^3.0.0", "jsdom": "^26.0.0", - "vite": "^8.0.13", + "vite": "6.3.6", "vitest": "^3.2.3" } } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 7a6cb17ba0..6d4eab70bd 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -30,6 +30,7 @@ import { mainlandZAiDefaultModelId, fireworksDefaultModelId, vercelAiGatewayDefaultModelId, + zooGatewayDefaultModelId, minimaxDefaultModelId, mimoDefaultModelId, unboundDefaultModelId, @@ -93,6 +94,7 @@ import { ZAi, Fireworks, VercelAiGateway, + ZooGateway, MiniMax, Mimo, } from "./providers" @@ -133,7 +135,7 @@ const ApiOptions = ({ setErrorMessage, }: ApiOptionsProps) => { const { t } = useAppTranslation() - const { organizationAllowList, openAiCodexIsAuthenticated } = useExtensionState() + const { organizationAllowList, openAiCodexIsAuthenticated, zooCodeIsAuthenticated } = useExtensionState() const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { const headers = apiConfiguration?.openAiHeaders || {} @@ -270,9 +272,17 @@ const ApiOptions = ({ apiConfiguration, routerModels, organizationAllowList, + zooCodeIsAuthenticated, ) setErrorMessage(apiValidationResult) - }, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider]) + }, [ + apiConfiguration, + routerModels, + organizationAllowList, + setErrorMessage, + isRetiredSelectedProvider, + zooCodeIsAuthenticated, + ]) const onProviderChange = useCallback( (value: ProviderName) => { @@ -363,6 +373,7 @@ const ApiOptions = ({ fireworks: { field: "apiModelId", default: fireworksDefaultModelId }, poe: { field: "apiModelId", default: poeDefaultModelId }, "vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId }, + "zoo-gateway": { field: "zooGatewayModelId", default: zooGatewayDefaultModelId }, openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, lmstudio: { field: "lmStudioModelId" }, @@ -688,6 +699,17 @@ const ApiOptions = ({ /> )} + {selectedProvider === "zoo-gateway" && ( + + )} + {selectedProvider === "fireworks" && ( { expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiModelId", openAiCodexDefaultModelId, false) }) + it("initializes zooGatewayModelId to its default when switching provider to zoo-gateway", () => { + // Regression: zoo-gateway was previously missing from PROVIDER_MODEL_CONFIG, so switching + // providers never seeded zooGatewayModelId. Configs were left without a model id, which + // blocked completion flows that require a dynamic-provider model id. + const mockSetApiConfigurationField = vi.fn() + + renderApiOptions({ + apiConfiguration: { + apiProvider: "anthropic", + // No prior zooGatewayModelId. + }, + setApiConfigurationField: mockSetApiConfigurationField, + }) + + const providerSelectContainer = screen.getByTestId("provider-select") + const providerSelect = providerSelectContainer.querySelector("select") as HTMLSelectElement + fireEvent.change(providerSelect, { target: { value: "zoo-gateway" } }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiProvider", "zoo-gateway") + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("zooGatewayModelId", zooGatewayDefaultModelId, false) + }) + it("shows temperature and rate limit controls by default", () => { renderApiOptions({ apiConfiguration: {}, diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 2d5051fb77..0e0871f56d 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -63,6 +63,7 @@ export const PROVIDERS = [ { value: "zai", label: "Z.ai", proxy: false }, { value: "fireworks", label: "Fireworks AI", proxy: false }, { value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false }, + { value: "zoo-gateway", label: "Zoo Gateway", proxy: false }, { value: "minimax", label: "MiniMax", proxy: false }, { value: "mimo", label: "Xiaomi MiMo", proxy: false }, { value: "baseten", label: "Baseten", proxy: false }, diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx new file mode 100644 index 0000000000..bb9e62addd --- /dev/null +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -0,0 +1,83 @@ +import { + type ProviderSettings, + type OrganizationAllowList, + type RouterModels, + zooGatewayDefaultModelId, +} from "@roo-code/types" + +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { getZooCodeAuthUrl } from "@src/oauth/urls" +import { useAppTranslation } from "@src/i18n/TranslationContext" + +import { ModelPicker } from "../ModelPicker" + +type ZooGatewayProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const ZooGateway = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, + simplifySettings, +}: ZooGatewayProps) => { + const { t } = useAppTranslation() + const { zooCodeIsAuthenticated, zooCodeUserEmail, zooCodeUserName, zooCodeBaseUrl, uriScheme, deviceName } = + useExtensionState() + + const authUrl = getZooCodeAuthUrl(uriScheme, zooCodeBaseUrl, deviceName) + + return ( + <> + {/* Zoo Code Authentication Section */} +
+
+ + {zooCodeIsAuthenticated && zooCodeUserEmail && ( + {zooCodeUserEmail} + )} +
+ {!zooCodeIsAuthenticated ? ( +
+

+ {t("settings:providers.zooGateway.signInDescription")} +

+ + {t("settings:providers.zooGateway.signInButton")} + +
+ ) : ( +
+ + + {zooCodeUserName + ? t("settings:providers.zooGateway.authenticatedAs", { name: zooCodeUserName }) + : t("settings:providers.zooGateway.authenticated")} + +
+ )} +
+ + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 02a928ffb5..6811e13154 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -22,6 +22,7 @@ export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" export { VercelAiGateway } from "./VercelAiGateway" +export { ZooGateway } from "./ZooGateway" export { MiniMax } from "./MiniMax" export { Mimo } from "./Mimo" export { Baseten } from "./Baseten" diff --git a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx index 7667d32ad6..fdde35320f 100644 --- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx +++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx @@ -36,7 +36,8 @@ const getWelcomeApiConfiguration = (apiConfiguration?: ProviderSettings): Provid } const WelcomeViewProvider = () => { - const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState() + const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, zooCodeIsAuthenticated } = + useExtensionState() const { t } = useAppTranslation() const [errorMessage, setErrorMessage] = useState(undefined) const [showProviderSetup, setShowProviderSetup] = useState(false) @@ -65,7 +66,7 @@ const WelcomeViewProvider = () => { return } - const error = validateApiConfiguration(effectiveApiConfiguration) + const error = validateApiConfiguration(effectiveApiConfiguration, undefined, undefined, zooCodeIsAuthenticated) if (error) { setErrorMessage(error) @@ -78,7 +79,14 @@ const WelcomeViewProvider = () => { text: currentApiConfigName, apiConfiguration: effectiveApiConfiguration, }) - }, [showProviderSetup, apiConfiguration, setApiConfiguration, effectiveApiConfiguration, currentApiConfigName]) + }, [ + showProviderSetup, + apiConfiguration, + setApiConfiguration, + effectiveApiConfiguration, + currentApiConfigName, + zooCodeIsAuthenticated, + ]) if (!showProviderSetup) { return ( diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index d12335d9aa..316e3f66ac 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -452,7 +452,6 @@ "step3": "3. O crear un compte de servei amb credencials." }, "googleCloudCredentials": "Credencials de Google Cloud", - "googleCloudCredentialsPathWarning": "Aquest camp espera el contingut JSON d'un fitxer de clau de compte de servei, no una ruta. Si tens una ruta, enganxa-la al camp Ruta del fitxer de clau de Google Cloud a continuació, o buida aquest camp i utilitza la variable d'entorn GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Ruta del fitxer de clau de Google Cloud", "googleCloudProjectId": "ID del projecte de Google Cloud", "googleCloudRegion": "Regió de Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Per defecte: claude", "maxTokensLabel": "Tokens màxims de sortida", "maxTokensDescription": "Nombre màxim de tokens de sortida per a les respostes de Claude Code. El valor per defecte és 8000." + }, + "zooGateway": { + "account": "Compte de Zoo Code", + "signInButton": "Iniciar sessió a Zoo Code", + "signInDescription": "Inicia sessió per utilitzar Zoo Gateway amb el teu compte", + "authenticated": "Autenticat", + "authenticatedAs": "Autenticat com a {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "L'extensió obté automàticament la llista més recent de models disponibles a {{serviceName}}. Si no esteu segur de quin model triar, Zoo Code funciona millor amb {{defaultModelId}}. També podeu cercar \"free\" per a opcions gratuïtes actualment disponibles.", + "automaticFetch": "L'extensió obté automàticament la llista més recent de models disponibles a {{serviceName}}. Si no esteu segur de quin model triar, Zoo Code funciona millor amb {{defaultModelId}}.", "label": "Model", "searchPlaceholder": "Cerca", "noMatchFound": "No s'ha trobat cap coincidència", @@ -899,7 +905,8 @@ "providerNotAllowed": "El proveïdor '{{provider}}' no està permès per la vostra organització", "modelNotAllowed": "El model '{{model}}' no està permès per al proveïdor '{{provider}}' per la vostra organització", "profileInvalid": "Aquest perfil conté un proveïdor o model que no està permès per la vostra organització", - "qwenCodeOauthPath": "Has de proporcionar una ruta vàlida de credencials OAuth" + "qwenCodeOauthPath": "Has de proporcionar una ruta vàlida de credencials OAuth", + "zooGatewaySignIn": "Has d'iniciar sessió a Zoo Code per utilitzar Zoo Gateway. Fes clic a 'Inicia sessió' per autenticar-te." }, "placeholders": { "apiKey": "Introduïu la clau API...", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 8e778de4ad..7fbb2021d6 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -452,7 +452,6 @@ "step3": "3. Oder ein Servicekonto mit Anmeldeinformationen erstellen." }, "googleCloudCredentials": "Google Cloud Anmeldedaten", - "googleCloudCredentialsPathWarning": "Dieses Feld erwartet den JSON-Inhalt einer Dienstkonto-Schlüsseldatei, keinen Pfad. Wenn Sie einen Pfad haben, fügen Sie ihn unten in das Feld Google Cloud Schlüsseldateipfad ein, oder leeren Sie dieses Feld und verwenden Sie die Umgebungsvariable GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Google Cloud Schlüsseldateipfad", "googleCloudProjectId": "Google Cloud Projekt-ID", "googleCloudRegion": "Google Cloud Region", @@ -558,6 +557,13 @@ "placeholder": "Standard: claude", "maxTokensLabel": "Maximale Ausgabe-Tokens", "maxTokensDescription": "Maximale Anzahl an Ausgabe-Tokens für Claude Code-Antworten. Standard ist 8000." + }, + "zooGateway": { + "account": "Zoo Code Konto", + "signInButton": "Bei Zoo Code anmelden", + "signInDescription": "Melde dich an, um Zoo Gateway mit deinem Konto zu nutzen", + "authenticated": "Authentifiziert", + "authenticatedAs": "Authentifiziert als {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Die Erweiterung ruft automatisch die neueste Liste der auf {{serviceName}} verfügbaren Modelle ab. Wenn du dir nicht sicher bist, welches Modell du wählen sollst, funktioniert Zoo Code am besten mit {{defaultModelId}}. Du kannst auch versuchen, nach \"kostenlos\" zu suchen, um die derzeit verfügbaren kostenlosen Optionen zu finden.", + "automaticFetch": "Die Erweiterung ruft automatisch die neueste Liste der auf {{serviceName}} verfügbaren Modelle ab. Wenn du dir nicht sicher bist, welches Modell du wählen sollst, funktioniert Zoo Code am besten mit {{defaultModelId}}.", "label": "Modell", "searchPlaceholder": "Suchen", "noMatchFound": "Keine Übereinstimmung gefunden", @@ -899,7 +905,8 @@ "providerNotAllowed": "Anbieter '{{provider}}' ist von deiner Organisation nicht erlaubt", "modelNotAllowed": "Modell '{{model}}' ist für Anbieter '{{provider}}' von deiner Organisation nicht erlaubt", "profileInvalid": "Dieses Profil enthält einen Anbieter oder ein Modell, das von deiner Organisation nicht erlaubt ist", - "qwenCodeOauthPath": "Du musst einen gültigen OAuth-Anmeldedaten-Pfad angeben" + "qwenCodeOauthPath": "Du musst einen gültigen OAuth-Anmeldedaten-Pfad angeben", + "zooGatewaySignIn": "Du musst dich bei Zoo Code anmelden, um Zoo Gateway zu verwenden. Klicke auf 'Anmelden', um dich zu authentifizieren." }, "placeholders": { "apiKey": "API-Schlüssel eingeben...", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index bafb74393d..c0591680bd 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -515,7 +515,6 @@ "step3": "3. Or create a service account with credentials." }, "googleCloudCredentials": "Google Cloud Credentials", - "googleCloudCredentialsPathWarning": "This field expects the JSON contents of a service-account key file, not a path. If you have a path, paste it into the Google Cloud Key File Path field below, or clear this field and use the GOOGLE_APPLICATION_CREDENTIALS environment variable.", "googleCloudKeyFile": "Google Cloud Key File Path", "googleCloudProjectId": "Google Cloud Project ID", "googleCloudRegion": "Google Cloud Region", @@ -546,6 +545,13 @@ "learnMore": "Learn more about provider routing" } }, + "zooGateway": { + "account": "Zoo Code Account", + "signInButton": "Sign in to Zoo Code", + "signInDescription": "Sign in to use Zoo Gateway with your account", + "authenticated": "Authenticated", + "authenticatedAs": "Authenticated as {{name}}" + }, "customModel": { "capabilities": "Configure the capabilities and pricing for your custom OpenAI-compatible model. Be careful when specifying the model capabilities, as they can affect how Zoo Code performs.", "maxTokens": { @@ -924,7 +930,7 @@ } }, "modelPicker": { - "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Zoo Code works best with {{defaultModelId}}. You can also try searching \"free\" for no-cost options currently available.", + "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Zoo Code works best with {{defaultModelId}}.", "label": "Model", "searchPlaceholder": "Search", "noMatchFound": "No match found", @@ -962,7 +968,8 @@ "providerNotAllowed": "Provider '{{provider}}' is not allowed by your organization", "modelNotAllowed": "Model '{{model}}' is not allowed for provider '{{provider}}' by your organization", "profileInvalid": "This profile contains a provider or model that is not allowed by your organization", - "qwenCodeOauthPath": "You must provide a valid OAuth credentials path." + "qwenCodeOauthPath": "You must provide a valid OAuth credentials path.", + "zooGatewaySignIn": "You must sign in to Zoo Code to use Zoo Gateway. Click 'Sign In' to authenticate." }, "placeholders": { "apiKey": "Enter API Key...", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index aa5770bfe0..06f4fc3093 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -452,7 +452,6 @@ "step3": "3. O crear una cuenta de servicio con credenciales." }, "googleCloudCredentials": "Credenciales de Google Cloud", - "googleCloudCredentialsPathWarning": "Este campo espera el contenido JSON de un archivo de clave de cuenta de servicio, no una ruta. Si tienes una ruta, pégala en el campo Ruta del archivo de clave de Google Cloud a continuación, o borra este campo y utiliza la variable de entorno GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Ruta del archivo de clave de Google Cloud", "googleCloudProjectId": "ID del proyecto de Google Cloud", "googleCloudRegion": "Región de Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Por defecto: claude", "maxTokensLabel": "Tokens máximos de salida", "maxTokensDescription": "Número máximo de tokens de salida para las respuestas de Claude Code. El valor predeterminado es 8000." + }, + "zooGateway": { + "account": "Cuenta de Zoo Code", + "signInButton": "Iniciar sesión en Zoo Code", + "signInDescription": "Inicia sesión para usar Zoo Gateway con tu cuenta", + "authenticated": "Autenticado", + "authenticatedAs": "Autenticado como {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "La extensión obtiene automáticamente la lista más reciente de modelos disponibles en {{serviceName}}. Si no está seguro de qué modelo elegir, Zoo Code funciona mejor con {{defaultModelId}}. También puede buscar \"free\" para opciones sin costo actualmente disponibles.", + "automaticFetch": "La extensión obtiene automáticamente la lista más reciente de modelos disponibles en {{serviceName}}. Si no está seguro de qué modelo elegir, Zoo Code funciona mejor con {{defaultModelId}}.", "label": "Modelo", "searchPlaceholder": "Buscar", "noMatchFound": "No se encontraron coincidencias", @@ -899,7 +905,8 @@ "providerNotAllowed": "El proveedor '{{provider}}' no está permitido por su organización", "modelNotAllowed": "El modelo '{{model}}' no está permitido para el proveedor '{{provider}}' por su organización", "profileInvalid": "Este perfil contiene un proveedor o modelo que no está permitido por su organización", - "qwenCodeOauthPath": "Debes proporcionar una ruta válida de credenciales OAuth" + "qwenCodeOauthPath": "Debes proporcionar una ruta válida de credenciales OAuth", + "zooGatewaySignIn": "Debes iniciar sesión en Zoo Code para usar Zoo Gateway. Haz clic en 'Iniciar sesión' para autenticarte." }, "placeholders": { "apiKey": "Ingrese clave API...", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 98aa755809..1f98533162 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -452,7 +452,6 @@ "step3": "3. Ou créer un compte de service avec des identifiants." }, "googleCloudCredentials": "Identifiants Google Cloud", - "googleCloudCredentialsPathWarning": "Ce champ attend le contenu JSON d'un fichier de clé de compte de service, pas un chemin. Si vous disposez d'un chemin, collez-le dans le champ Chemin du fichier de clé Google Cloud ci-dessous, ou videz ce champ et utilisez la variable d'environnement GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Chemin du fichier de clé Google Cloud", "googleCloudProjectId": "ID du projet Google Cloud", "googleCloudRegion": "Région Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Défaut : claude", "maxTokensLabel": "Jetons de sortie max", "maxTokensDescription": "Nombre maximum de jetons de sortie pour les réponses de Claude Code. La valeur par défaut est 8000." + }, + "zooGateway": { + "account": "Compte Zoo Code", + "signInButton": "Se connecter à Zoo Code", + "signInDescription": "Connectez-vous pour utiliser Zoo Gateway avec votre compte", + "authenticated": "Authentifié", + "authenticatedAs": "Authentifié en tant que {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "L'extension récupère automatiquement la liste la plus récente des modèles disponibles sur {{serviceName}}. Si vous ne savez pas quel modèle choisir, Zoo Code fonctionne mieux avec {{defaultModelId}}. Vous pouvez également rechercher \"free\" pour les options gratuites actuellement disponibles.", + "automaticFetch": "L'extension récupère automatiquement la liste la plus récente des modèles disponibles sur {{serviceName}}. Si vous ne savez pas quel modèle choisir, Zoo Code fonctionne mieux avec {{defaultModelId}}.", "label": "Modèle", "searchPlaceholder": "Rechercher", "noMatchFound": "Aucune correspondance trouvée", @@ -899,7 +905,8 @@ "providerNotAllowed": "Le fournisseur '{{provider}}' n'est pas autorisé par votre organisation", "modelNotAllowed": "Le modèle '{{model}}' n'est pas autorisé pour le fournisseur '{{provider}}' par votre organisation", "profileInvalid": "Ce profil contient un fournisseur ou un modèle qui n'est pas autorisé par votre organisation", - "qwenCodeOauthPath": "Tu dois fournir un chemin valide pour les identifiants OAuth" + "qwenCodeOauthPath": "Tu dois fournir un chemin valide pour les identifiants OAuth", + "zooGatewaySignIn": "Tu dois te connecter à Zoo Code pour utiliser Zoo Gateway. Clique sur 'Se connecter' pour t'authentifier." }, "placeholders": { "apiKey": "Saisissez la clé API...", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0636d01a17..b06fa07c94 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -452,7 +452,6 @@ "step3": "3. या क्रेडेंशियल्स के साथ एक सर्विस अकाउंट बनाएं।" }, "googleCloudCredentials": "Google Cloud क्रेडेंशियल्स", - "googleCloudCredentialsPathWarning": "इस फ़ील्ड में सर्विस-अकाउंट कुंजी फ़ाइल की JSON सामग्री अपेक्षित है, पथ नहीं। यदि आपके पास पथ है, तो उसे नीचे दिए गए Google Cloud कुंजी फ़ाइल पथ फ़ील्ड में पेस्ट करें, या इस फ़ील्ड को खाली करें और GOOGLE_APPLICATION_CREDENTIALS पर्यावरण चर का उपयोग करें।", "googleCloudKeyFile": "Google Cloud कुंजी फ़ाइल पथ", "googleCloudProjectId": "Google Cloud प्रोजेक्ट ID", "googleCloudRegion": "Google Cloud क्षेत्र", @@ -558,6 +557,13 @@ "placeholder": "डिफ़ॉल्ट: claude", "maxTokensLabel": "अधिकतम आउटपुट टोकन", "maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।" + }, + "zooGateway": { + "account": "Zoo Code खाता", + "signInButton": "Zoo Code में साइन इन करें", + "signInDescription": "अपने खाते के साथ Zoo Gateway का उपयोग करने के लिए साइन इन करें", + "authenticated": "प्रमाणित", + "authenticatedAs": "{{name}} के रूप में प्रमाणित" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है। आप वर्तमान में उपलब्ध निःशुल्क विकल्पों के लिए \"free\" भी खोज सकते हैं।", + "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है।", "label": "मॉडल", "searchPlaceholder": "खोजें", "noMatchFound": "कोई मिलान नहीं मिला", @@ -899,7 +905,8 @@ "providerNotAllowed": "प्रदाता '{{provider}}' आपके संगठन द्वारा अनुमत नहीं है", "modelNotAllowed": "मॉडल '{{model}}' प्रदाता '{{provider}}' के लिए आपके संगठन द्वारा अनुमत नहीं है", "profileInvalid": "इस प्रोफ़ाइल में एक प्रदाता या मॉडल शामिल है जो आपके संगठन द्वारा अनुमत नहीं है", - "qwenCodeOauthPath": "आपको एक वैध OAuth क्रेडेंशियल पथ प्रदान करना होगा" + "qwenCodeOauthPath": "आपको एक वैध OAuth क्रेडेंशियल पथ प्रदान करना होगा", + "zooGatewaySignIn": "Zoo Gateway का उपयोग करने के लिए आपको Zoo Code में साइन इन करना होगा। प्रमाणीकृत करने के लिए 'साइन इन' पर क्लिक करें।" }, "placeholders": { "apiKey": "API कुंजी दर्ज करें...", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index b879bf0c36..bc30c6e414 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -452,7 +452,6 @@ "step3": "3. Atau buat service account dengan credentials." }, "googleCloudCredentials": "Google Cloud Credentials", - "googleCloudCredentialsPathWarning": "Bidang ini mengharapkan konten JSON dari file kunci akun layanan, bukan path. Jika Anda memiliki path, tempelkan ke bidang Path File Key Google Cloud di bawah, atau kosongkan bidang ini dan gunakan variabel lingkungan GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Path File Key Google Cloud", "googleCloudProjectId": "Google Cloud Project ID", "googleCloudRegion": "Google Cloud Region", @@ -558,6 +557,13 @@ "placeholder": "Default: claude", "maxTokensLabel": "Token Output Maks", "maxTokensDescription": "Jumlah maksimum token output untuk respons Claude Code. Default adalah 8000." + }, + "zooGateway": { + "account": "Akun Zoo Code", + "signInButton": "Masuk ke Zoo Code", + "signInDescription": "Masuk untuk menggunakan Zoo Gateway dengan akun Anda", + "authenticated": "Terautentikasi", + "authenticatedAs": "Terautentikasi sebagai {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Ekstensi secara otomatis mengambil daftar model terbaru yang tersedia di {{serviceName}}. Jika kamu tidak yakin model mana yang harus dipilih, Zoo Code bekerja terbaik dengan {{defaultModelId}}. Kamu juga dapat mencoba mencari \"free\" untuk opsi tanpa biaya yang saat ini tersedia.", + "automaticFetch": "Ekstensi secara otomatis mengambil daftar model terbaru yang tersedia di {{serviceName}}. Jika kamu tidak yakin model mana yang harus dipilih, Zoo Code bekerja terbaik dengan {{defaultModelId}}.", "label": "Model", "searchPlaceholder": "Cari", "noMatchFound": "Tidak ada yang cocok ditemukan", @@ -899,7 +905,8 @@ "providerNotAllowed": "Provider '{{provider}}' tidak diizinkan oleh organisasi kamu", "modelNotAllowed": "Model '{{model}}' tidak diizinkan untuk provider '{{provider}}' oleh organisasi kamu", "profileInvalid": "Profil ini berisi provider atau model yang tidak diizinkan oleh organisasi kamu", - "qwenCodeOauthPath": "Kamu harus memberikan jalur kredensial OAuth yang valid" + "qwenCodeOauthPath": "Kamu harus memberikan jalur kredensial OAuth yang valid", + "zooGatewaySignIn": "Kamu harus masuk ke Zoo Code untuk menggunakan Zoo Gateway. Klik 'Masuk' untuk mengautentikasi." }, "placeholders": { "apiKey": "Masukkan API Key...", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index c8d8f41a42..39daaedb69 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -452,7 +452,6 @@ "step3": "3. Oppure creare un account di servizio con credenziali." }, "googleCloudCredentials": "Credenziali Google Cloud", - "googleCloudCredentialsPathWarning": "Questo campo si aspetta il contenuto JSON di un file di chiave di account di servizio, non un percorso. Se hai un percorso, incollalo nel campo Percorso file chiave Google Cloud qui sotto, oppure svuota questo campo e usa la variabile d'ambiente GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Percorso file chiave Google Cloud", "googleCloudProjectId": "ID progetto Google Cloud", "googleCloudRegion": "Regione Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Predefinito: claude", "maxTokensLabel": "Token di output massimi", "maxTokensDescription": "Numero massimo di token di output per le risposte di Claude Code. Il valore predefinito è 8000." + }, + "zooGateway": { + "account": "Account Zoo Code", + "signInButton": "Accedi a Zoo Code", + "signInDescription": "Accedi per utilizzare Zoo Gateway con il tuo account", + "authenticated": "Autenticato", + "authenticatedAs": "Autenticato come {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "L'estensione recupera automaticamente l'elenco più recente dei modelli disponibili su {{serviceName}}. Se non sei sicuro di quale modello scegliere, Zoo Code funziona meglio con {{defaultModelId}}. Puoi anche cercare \"free\" per opzioni gratuite attualmente disponibili.", + "automaticFetch": "L'estensione recupera automaticamente l'elenco più recente dei modelli disponibili su {{serviceName}}. Se non sei sicuro di quale modello scegliere, Zoo Code funziona meglio con {{defaultModelId}}.", "label": "Modello", "searchPlaceholder": "Cerca", "noMatchFound": "Nessuna corrispondenza trovata", @@ -899,7 +905,8 @@ "providerNotAllowed": "Il fornitore '{{provider}}' non è consentito dalla tua organizzazione", "modelNotAllowed": "Il modello '{{model}}' non è consentito per il fornitore '{{provider}}' dalla tua organizzazione.", "profileInvalid": "Questo profilo contiene un fornitore o un modello non consentito dalla tua organizzazione.", - "qwenCodeOauthPath": "Devi fornire un percorso valido per le credenziali OAuth" + "qwenCodeOauthPath": "Devi fornire un percorso valido per le credenziali OAuth", + "zooGatewaySignIn": "Devi accedere a Zoo Code per utilizzare Zoo Gateway. Clicca su 'Accedi' per autenticarti." }, "placeholders": { "apiKey": "Inserisci chiave API...", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 7a149ce6f9..656f9846c4 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -452,7 +452,6 @@ "step3": "3. または、認証情報付きのサービスアカウントを作成します。" }, "googleCloudCredentials": "Google Cloud認証情報", - "googleCloudCredentialsPathWarning": "このフィールドは、パスではなく、サービスアカウントキーファイルのJSON内容を期待します。パスをお持ちの場合は、下のGoogle Cloudキーファイルパスフィールドに貼り付けるか、このフィールドをクリアしてGOOGLE_APPLICATION_CREDENTIALS環境変数を使用してください。", "googleCloudKeyFile": "Google Cloudキーファイルパス", "googleCloudProjectId": "Google Cloudプロジェクトid", "googleCloudRegion": "Google Cloudリージョン", @@ -558,6 +557,13 @@ "placeholder": "デフォルト:claude", "maxTokensLabel": "最大出力トークン", "maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。" + }, + "zooGateway": { + "account": "Zoo Code アカウント", + "signInButton": "Zoo Code にサインイン", + "signInDescription": "Zoo Gateway をアカウントで使用するにはサインインしてください", + "authenticated": "認証済み", + "authenticatedAs": "{{name}} として認証済み" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。また、「free」で検索すると、現在利用可能な無料オプションを見つけることができます。", + "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。", "label": "モデル", "searchPlaceholder": "検索", "noMatchFound": "一致するものが見つかりません", @@ -899,7 +905,8 @@ "providerNotAllowed": "プロバイダー「{{provider}}」は組織によって許可されていません", "modelNotAllowed": "モデル「{{model}}」はプロバイダー「{{provider}}」に対して組織によって許可されていません", "profileInvalid": "このプロファイルには、組織によって許可されていないプロバイダーまたはモデルが含まれています", - "qwenCodeOauthPath": "有効なOAuth認証情報のパスを提供する必要があります" + "qwenCodeOauthPath": "有効なOAuth認証情報のパスを提供する必要があります", + "zooGatewaySignIn": "Zoo Gatewayを使用するにはZoo Codeにサインインする必要があります。認証するには「サインイン」をクリックしてください。" }, "placeholders": { "apiKey": "API キーを入力...", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 0231abf317..3e054d28d0 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -452,7 +452,6 @@ "step3": "3. 또는 자격 증명이 있는 서비스 계정을 만드세요." }, "googleCloudCredentials": "Google Cloud 자격 증명", - "googleCloudCredentialsPathWarning": "이 필드는 경로가 아닌 서비스 계정 키 파일의 JSON 내용을 기대합니다. 경로가 있다면 아래의 Google Cloud 키 파일 경로 필드에 붙여넣거나, 이 필드를 비우고 GOOGLE_APPLICATION_CREDENTIALS 환경 변수를 사용하세요.", "googleCloudKeyFile": "Google Cloud 키 파일 경로", "googleCloudProjectId": "Google Cloud 프로젝트 ID", "googleCloudRegion": "Google Cloud 리전", @@ -558,6 +557,13 @@ "placeholder": "기본값: claude", "maxTokensLabel": "최대 출력 토큰", "maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다." + }, + "zooGateway": { + "account": "Zoo Code 계정", + "signInButton": "Zoo Code에 로그인", + "signInDescription": "계정으로 Zoo Gateway를 사용하려면 로그인하세요", + "authenticated": "인증됨", + "authenticatedAs": "{{name}}(으)로 인증됨" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다. 현재 사용 가능한 무료 옵션을 찾으려면 \"free\"를 검색해 볼 수도 있습니다.", + "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다.", "label": "모델", "searchPlaceholder": "검색", "noMatchFound": "일치하는 항목 없음", @@ -899,7 +905,8 @@ "providerNotAllowed": "제공자 '{{provider}}'는 조직에서 허용되지 않습니다", "modelNotAllowed": "모델 '{{model}}'은 제공자 '{{provider}}'에 대해 조직에서 허용되지 않습니다", "profileInvalid": "이 프로필에는 조직에서 허용되지 않는 제공자 또는 모델이 포함되어 있습니다", - "qwenCodeOauthPath": "유효한 OAuth 자격 증명 경로를 제공해야 합니다" + "qwenCodeOauthPath": "유효한 OAuth 자격 증명 경로를 제공해야 합니다", + "zooGatewaySignIn": "Zoo Gateway를 사용하려면 Zoo Code에 로그인해야 합니다. 인증하려면 '로그인'을 클릭하세요." }, "placeholders": { "apiKey": "API 키 입력...", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 7dce57c344..0744ef9d32 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -452,7 +452,6 @@ "step3": "3. Of maak een serviceaccount met referenties." }, "googleCloudCredentials": "Google Cloud-referenties", - "googleCloudCredentialsPathWarning": "Dit veld verwacht de JSON-inhoud van een serviceaccount-sleutelbestand, geen pad. Als je een pad hebt, plak het dan in het veld Google Cloud-sleutelbestandspad hieronder, of leeg dit veld en gebruik de omgevingsvariabele GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Google Cloud-sleutelbestandspad", "googleCloudProjectId": "Google Cloud-project-ID", "googleCloudRegion": "Google Cloud-regio", @@ -558,6 +557,13 @@ "placeholder": "Standaard: claude", "maxTokensLabel": "Max Output Tokens", "maxTokensDescription": "Maximaal aantal output-tokens voor Claude Code-reacties. Standaard is 8000." + }, + "zooGateway": { + "account": "Zoo Code Account", + "signInButton": "Inloggen bij Zoo Code", + "signInDescription": "Log in om Zoo Gateway met je account te gebruiken", + "authenticated": "Geauthenticeerd", + "authenticatedAs": "Geauthenticeerd als {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "De extensie haalt automatisch de nieuwste lijst met modellen op van {{serviceName}}. Weet je niet welk model je moet kiezen? Zoo Code werkt het beste met {{defaultModelId}}. Je kunt ook zoeken op 'free' voor gratis opties die nu beschikbaar zijn.", + "automaticFetch": "De extensie haalt automatisch de nieuwste lijst met modellen op van {{serviceName}}. Weet je niet welk model je moet kiezen? Zoo Code werkt het beste met {{defaultModelId}}.", "label": "Model", "searchPlaceholder": "Zoeken", "noMatchFound": "Geen overeenkomsten gevonden", @@ -899,7 +905,8 @@ "providerNotAllowed": "Provider '{{provider}}' is niet toegestaan door je organisatie", "modelNotAllowed": "Model '{{model}}' is niet toegestaan voor provider '{{provider}}' door je organisatie", "profileInvalid": "Dit profiel bevat een provider of model dat niet is toegestaan door je organisatie", - "qwenCodeOauthPath": "Je moet een geldig OAuth-referentiepad opgeven" + "qwenCodeOauthPath": "Je moet een geldig OAuth-referentiepad opgeven", + "zooGatewaySignIn": "Je moet inloggen bij Zoo Code om Zoo Gateway te gebruiken. Klik op 'Inloggen' om je te authenticeren." }, "placeholders": { "apiKey": "Voer API-sleutel in...", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 5190e94e7b..71fc221944 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -452,7 +452,6 @@ "step3": "3. Lub utworzyć konto usługi z poświadczeniami." }, "googleCloudCredentials": "Poświadczenia Google Cloud", - "googleCloudCredentialsPathWarning": "To pole oczekuje zawartości JSON pliku klucza konta usługi, a nie ścieżki. Jeśli masz ścieżkę, wklej ją do pola Ścieżka pliku klucza Google Cloud poniżej lub wyczyść to pole i użyj zmiennej środowiskowej GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Ścieżka pliku klucza Google Cloud", "googleCloudProjectId": "ID projektu Google Cloud", "googleCloudRegion": "Region Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Domyślnie: claude", "maxTokensLabel": "Maksymalna liczba tokenów wyjściowych", "maxTokensDescription": "Maksymalna liczba tokenów wyjściowych dla odpowiedzi Claude Code. Domyślnie 8000." + }, + "zooGateway": { + "account": "Konto Zoo Code", + "signInButton": "Zaloguj się do Zoo Code", + "signInDescription": "Zaloguj się, aby korzystać z Zoo Gateway ze swoim kontem", + "authenticated": "Uwierzytelniono", + "authenticatedAs": "Uwierzytelniono jako {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Rozszerzenie automatycznie pobiera najnowszą listę modeli dostępnych w {{serviceName}}. Jeśli nie jesteś pewien, który model wybrać, Zoo Code działa najlepiej z {{defaultModelId}}. Możesz również wyszukać \"free\", aby znaleźć obecnie dostępne opcje bezpłatne.", + "automaticFetch": "Rozszerzenie automatycznie pobiera najnowszą listę modeli dostępnych w {{serviceName}}. Jeśli nie jesteś pewien, który model wybrać, Zoo Code działa najlepiej z {{defaultModelId}}.", "label": "Model", "searchPlaceholder": "Wyszukaj", "noMatchFound": "Nie znaleziono dopasowań", @@ -899,7 +905,8 @@ "providerNotAllowed": "Dostawca '{{provider}}' nie jest dozwolony przez Twoją organizację", "modelNotAllowed": "Model '{{model}}' nie jest dozwolony dla dostawcy '{{provider}}' przez Twoją organizację", "profileInvalid": "Ten profil zawiera dostawcę lub model, który nie jest dozwolony przez Twoją organizację", - "qwenCodeOauthPath": "Musisz podać prawidłową ścieżkę do poświadczeń OAuth" + "qwenCodeOauthPath": "Musisz podać prawidłową ścieżkę do poświadczeń OAuth", + "zooGatewaySignIn": "Musisz zalogować się do Zoo Code, aby korzystać z Zoo Gateway. Kliknij 'Zaloguj się', aby się uwierzytelnić." }, "placeholders": { "apiKey": "Wprowadź klucz API...", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 24daa058a1..fd2f065cd4 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -452,7 +452,6 @@ "step3": "3. Ou criar uma conta de serviço com credenciais." }, "googleCloudCredentials": "Credenciais Google Cloud", - "googleCloudCredentialsPathWarning": "Este campo espera o conteúdo JSON de um arquivo de chave de conta de serviço, não um caminho. Se você tiver um caminho, cole-o no campo Caminho do Arquivo de Chave Google Cloud abaixo, ou limpe este campo e use a variável de ambiente GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Caminho do Arquivo de Chave Google Cloud", "googleCloudProjectId": "ID do Projeto Google Cloud", "googleCloudRegion": "Região Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Padrão: claude", "maxTokensLabel": "Tokens de saída máximos", "maxTokensDescription": "Número máximo de tokens de saída para respostas do Claude Code. O padrão é 8000." + }, + "zooGateway": { + "account": "Conta Zoo Code", + "signInButton": "Entrar no Zoo Code", + "signInDescription": "Entre para usar o Zoo Gateway com sua conta", + "authenticated": "Autenticado", + "authenticatedAs": "Autenticado como {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "A extensão busca automaticamente a lista mais recente de modelos disponíveis em {{serviceName}}. Se você não tem certeza sobre qual modelo escolher, o Zoo Code funciona melhor com {{defaultModelId}}. Você também pode pesquisar por \"free\" para encontrar opções gratuitas atualmente disponíveis.", + "automaticFetch": "A extensão busca automaticamente a lista mais recente de modelos disponíveis em {{serviceName}}. Se você não tem certeza sobre qual modelo escolher, o Zoo Code funciona melhor com {{defaultModelId}}.", "label": "Modelo", "searchPlaceholder": "Pesquisar", "noMatchFound": "Nenhuma correspondência encontrada", @@ -899,7 +905,8 @@ "providerNotAllowed": "O provedor '{{provider}}' não é permitido pela sua organização", "modelNotAllowed": "O modelo '{{model}}' não é permitido para o provedor '{{provider}}' pela sua organização", "profileInvalid": "Este perfil contém um provedor ou modelo que não é permitido pela sua organização", - "qwenCodeOauthPath": "Você deve fornecer um caminho válido de credenciais OAuth" + "qwenCodeOauthPath": "Você deve fornecer um caminho válido de credenciais OAuth", + "zooGatewaySignIn": "Você deve fazer login no Zoo Code para usar o Zoo Gateway. Clique em 'Entrar' para autenticar." }, "placeholders": { "apiKey": "Digite a chave API...", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 96abdb0d0a..3772f8c749 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -452,7 +452,6 @@ "step3": "3. Или создайте сервисный аккаунт с ключом." }, "googleCloudCredentials": "Учётные данные Google Cloud", - "googleCloudCredentialsPathWarning": "Это поле ожидает JSON-содержимое файла ключа сервисного аккаунта, а не путь. Если у вас есть путь, вставьте его в поле Путь к ключу Google Cloud ниже, либо очистите это поле и используйте переменную окружения GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Путь к ключу Google Cloud", "googleCloudProjectId": "ID проекта Google Cloud", "googleCloudRegion": "Регион Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "По умолчанию: claude", "maxTokensLabel": "Макс. выходных токенов", "maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000." + }, + "zooGateway": { + "account": "Учетная запись Zoo Code", + "signInButton": "Войти в Zoo Code", + "signInDescription": "Войдите, чтобы использовать Zoo Gateway с вашей учетной записью", + "authenticated": "Аутентифицирован", + "authenticatedAs": "Аутентифицирован как {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}. Также попробуйте поискать \"free\" для бесплатных вариантов.", + "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}.", "label": "Модель", "searchPlaceholder": "Поиск", "noMatchFound": "Совпадений не найдено", @@ -899,7 +905,8 @@ "providerNotAllowed": "Провайдер '{{provider}}' не разрешен вашей организацией", "modelNotAllowed": "Модель '{{model}}' не разрешена для провайдера '{{provider}}' вашей организацией", "profileInvalid": "Этот профиль содержит провайдера или модель, которые не разрешены вашей организацией", - "qwenCodeOauthPath": "Вы должны указать допустимый путь к учетным данным OAuth" + "qwenCodeOauthPath": "Вы должны указать допустимый путь к учетным данным OAuth", + "zooGatewaySignIn": "Для использования Zoo Gateway необходимо войти в Zoo Code. Нажмите «Войти», чтобы пройти аутентификацию." }, "placeholders": { "apiKey": "Введите API-ключ...", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 8bb76737ac..d912f9efbd 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -452,7 +452,6 @@ "step3": "3. Veya kimlik bilgileriyle bir hizmet hesabı oluşturun." }, "googleCloudCredentials": "Google Cloud Kimlik Bilgileri", - "googleCloudCredentialsPathWarning": "Bu alan, bir yol değil, bir hizmet hesabı anahtar dosyasının JSON içeriğini bekler. Bir yolunuz varsa, aşağıdaki Google Cloud Anahtar Dosyası Yolu alanına yapıştırın veya bu alanı temizleyin ve GOOGLE_APPLICATION_CREDENTIALS ortam değişkenini kullanın.", "googleCloudKeyFile": "Google Cloud Anahtar Dosyası Yolu", "googleCloudProjectId": "Google Cloud Proje Kimliği", "googleCloudRegion": "Google Cloud Bölgesi", @@ -558,6 +557,13 @@ "placeholder": "Varsayılan: claude", "maxTokensLabel": "Maksimum Çıktı Token sayısı", "maxTokensDescription": "Claude Code yanıtları için maksimum çıktı token sayısı. Varsayılan 8000'dir." + }, + "zooGateway": { + "account": "Zoo Code Hesabı", + "signInButton": "Zoo Code'a giriş yap", + "signInDescription": "Hesabınızla Zoo Gateway kullanmak için giriş yapın", + "authenticated": "Kimlik doğrulandı", + "authenticatedAs": "{{name}} olarak kimlik doğrulandı" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Uzantı {{serviceName}} üzerinde bulunan mevcut modellerin en güncel listesini otomatik olarak alır. Hangi modeli seçeceğinizden emin değilseniz, Zoo Code {{defaultModelId}} ile en iyi şekilde çalışır. Şu anda mevcut olan ücretsiz seçenekleri bulmak için \"free\" araması da yapabilirsiniz.", + "automaticFetch": "Uzantı {{serviceName}} üzerinde bulunan mevcut modellerin en güncel listesini otomatik olarak alır. Hangi modeli seçeceğinizden emin değilseniz, Zoo Code {{defaultModelId}} ile en iyi şekilde çalışır.", "label": "Model", "searchPlaceholder": "Ara", "noMatchFound": "Eşleşme bulunamadı", @@ -899,7 +905,8 @@ "providerNotAllowed": "Sağlayıcı '{{provider}}' kuruluşunuz tarafından izin verilmiyor", "modelNotAllowed": "Model '{{model}}' sağlayıcı '{{provider}}' için kuruluşunuz tarafından izin verilmiyor", "profileInvalid": "Bu profil, kuruluşunuz tarafından izin verilmeyen bir sağlayıcı veya model içeriyor", - "qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısın" + "qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısınız", + "zooGatewaySignIn": "Zoo Gateway'i kullanmak için Zoo Code'a giriş yapmalısınız. Kimlik doğrulamak için 'Giriş Yap' düğmesine tıklayın." }, "placeholders": { "apiKey": "API anahtarını girin...", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index e44309cf7e..c383443bbf 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -452,7 +452,6 @@ "step3": "3. Hoặc tạo tài khoản dịch vụ với thông tin xác thực." }, "googleCloudCredentials": "Thông tin xác thực Google Cloud", - "googleCloudCredentialsPathWarning": "Trường này mong đợi nội dung JSON của tệp khóa tài khoản dịch vụ, không phải đường dẫn. Nếu bạn có đường dẫn, hãy dán vào trường Đường dẫn tệp khóa Google Cloud bên dưới, hoặc xóa trường này và sử dụng biến môi trường GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Đường dẫn tệp khóa Google Cloud", "googleCloudProjectId": "ID dự án Google Cloud", "googleCloudRegion": "Vùng Google Cloud", @@ -558,6 +557,13 @@ "placeholder": "Mặc định: claude", "maxTokensLabel": "Số token đầu ra tối đa", "maxTokensDescription": "Số lượng token đầu ra tối đa cho các phản hồi của Claude Code. Mặc định là 8000." + }, + "zooGateway": { + "account": "Tài khoản Zoo Code", + "signInButton": "Đăng nhập vào Zoo Code", + "signInDescription": "Đăng nhập để sử dụng Zoo Gateway với tài khoản của bạn", + "authenticated": "Đã xác thực", + "authenticatedAs": "Đã xác thực với tên {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Tiện ích mở rộng tự động lấy danh sách mới nhất các mô hình có sẵn trên {{serviceName}}. Nếu bạn không chắc chắn nên chọn mô hình nào, Zoo Code hoạt động tốt nhất với {{defaultModelId}}. Bạn cũng có thể thử tìm kiếm \"free\" cho các tùy chọn miễn phí hiện có.", + "automaticFetch": "Tiện ích mở rộng tự động lấy danh sách mới nhất các mô hình có sẵn trên {{serviceName}}. Nếu bạn không chắc chắn nên chọn mô hình nào, Zoo Code hoạt động tốt nhất với {{defaultModelId}}.", "label": "Mô hình", "searchPlaceholder": "Tìm kiếm", "noMatchFound": "Không tìm thấy kết quả", @@ -899,7 +905,8 @@ "providerNotAllowed": "Nhà cung cấp '{{provider}}' không được phép bởi tổ chức của bạn", "modelNotAllowed": "Mô hình '{{model}}' không được phép cho nhà cung cấp '{{provider}}' bởi tổ chức của bạn", "profileInvalid": "Hồ sơ này chứa một nhà cung cấp hoặc mô hình không được phép bởi tổ chức của bạn", - "qwenCodeOauthPath": "Bạn phải cung cấp đường dẫn thông tin xác thực OAuth hợp lệ" + "qwenCodeOauthPath": "Bạn phải cung cấp đường dẫn thông tin xác thực OAuth hợp lệ", + "zooGatewaySignIn": "Bạn phải đăng nhập vào Zoo Code để sử dụng Zoo Gateway. Nhấp vào 'Đăng nhập' để xác thực." }, "placeholders": { "apiKey": "Nhập khóa API...", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 54bd582dfd..9f5e32226f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -452,7 +452,6 @@ "step3": "3. 创建服务账号获取凭证" }, "googleCloudCredentials": "Google Cloud 凭证", - "googleCloudCredentialsPathWarning": "此字段需要服务账号密钥文件的 JSON 内容,而不是路径。如果您有路径,请将其粘贴到下方的Google Cloud 密钥文件路径字段中,或清除此字段并使用 GOOGLE_APPLICATION_CREDENTIALS 环境变量。", "googleCloudKeyFile": "Google Cloud 密钥文件路径", "googleCloudProjectId": "Google Cloud 项目 ID", "googleCloudRegion": "Google Cloud 区域", @@ -558,6 +557,13 @@ "placeholder": "默认:claude", "maxTokensLabel": "最大输出 Token", "maxTokensDescription": "Claude Code 响应的最大输出 Token 数量。默认为 8000。" + }, + "zooGateway": { + "account": "Zoo Code 账户", + "signInButton": "登录 Zoo Code", + "signInDescription": "登录以使用您的账户访问 Zoo Gateway", + "authenticated": "已认证", + "authenticatedAs": "已认证为 {{name}}" } }, "checkpoints": { @@ -861,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。您还可以搜索\"free\"以查找当前可用的免费选项。", + "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。", "label": "模型", "searchPlaceholder": "搜索", "noMatchFound": "未找到匹配项", @@ -899,7 +905,8 @@ "providerNotAllowed": "提供商 '{{provider}}' 不允许用于您的组织", "modelNotAllowed": "模型 '{{model}}' 不允许用于提供商 '{{provider}}',您的组织不允许", "profileInvalid": "此配置文件包含您的组织不允许的提供商或模型", - "qwenCodeOauthPath": "您必须提供有效的 OAuth 凭证路径" + "qwenCodeOauthPath": "您必须提供有效的 OAuth 凭证路径", + "zooGatewaySignIn": "您必须登录 Zoo Code 才能使用 Zoo Gateway。点击「登录」进行身份验证。" }, "placeholders": { "apiKey": "请输入 API 密钥...", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 8b81b7e9fc..309ed06bd1 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -462,7 +462,6 @@ "step3": "3. 或建立具有憑證的服務帳戶。" }, "googleCloudCredentials": "Google Cloud 憑證", - "googleCloudCredentialsPathWarning": "此欄位需要服務帳號金鑰檔案的 JSON 內容,而不是路徑。如果您有路徑,請將其貼到下方的Google Cloud 金鑰檔案路徑欄位,或清除此欄位並使用 GOOGLE_APPLICATION_CREDENTIALS 環境變數。", "googleCloudKeyFile": "Google Cloud 金鑰檔案路徑", "googleCloudProjectId": "Google Cloud 專案 ID", "googleCloudRegion": "Google Cloud 區域", @@ -568,6 +567,13 @@ "placeholder": "預設:claude", "maxTokensLabel": "最大輸出 Token", "maxTokensDescription": "Claude Code 回應的最大輸出 Token 數量。預設為 8000。" + }, + "zooGateway": { + "account": "Zoo Code 帳戶", + "signInButton": "登入 Zoo Code", + "signInDescription": "登入以使用您的帳戶存取 Zoo Gateway", + "authenticated": "已認證", + "authenticatedAs": "已認證為 {{name}}" } }, "checkpoints": { @@ -871,7 +877,7 @@ } }, "modelPicker": { - "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。您也可以搜尋「free」來檢視目前可用的免費選項。", + "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。", "label": "模型", "searchPlaceholder": "搜尋", "noMatchFound": "找不到符合的結果", @@ -909,7 +915,8 @@ "providerNotAllowed": "供應商 '{{provider}}' 不允許用於您的組織。", "modelNotAllowed": "模型 '{{model}}' 不允許用於供應商 '{{provider}}',此設定已被組織禁止", "profileInvalid": "此設定檔包含您的組織不允許的供應商或模型", - "qwenCodeOauthPath": "您必須提供有效的 OAuth 憑證路徑" + "qwenCodeOauthPath": "您必須提供有效的 OAuth 憑證路徑", + "zooGatewaySignIn": "您必須登入 Zoo Code 才能使用 Zoo Gateway。點擊「登入」進行身份驗證。" }, "placeholders": { "apiKey": "請輸入 API 金鑰...", diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index f506171acc..30c8af2d01 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -17,8 +17,9 @@ export function validateApiConfiguration( apiConfiguration: ProviderSettings, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, + zooCodeIsAuthenticated?: boolean, ): string | undefined { - const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated) if (keysAndIdsPresentErrorMessage) { return keysAndIdsPresentErrorMessage @@ -36,7 +37,10 @@ export function validateApiConfiguration( return validateDynamicProviderModelId(apiConfiguration, routerModels) } -function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): string | undefined { +function validateModelsAndKeysProvided( + apiConfiguration: ProviderSettings, + zooCodeIsAuthenticated?: boolean, +): string | undefined { switch (apiConfiguration.apiProvider) { case "openrouter": if (!apiConfiguration.openRouterApiKey) { @@ -123,6 +127,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "zoo-gateway": + if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { + return i18next.t("settings:validation.zooGatewaySignIn") + } + break case "baseten": if (!apiConfiguration.basetenApiKey) { return i18next.t("settings:validation.apiKey") @@ -282,8 +291,9 @@ export function validateApiConfigurationExcludingModelErrors( apiConfiguration: ProviderSettings, _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, + zooCodeIsAuthenticated?: boolean, ): string | undefined { - const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated) if (keysAndIdsPresentErrorMessage) { return keysAndIdsPresentErrorMessage diff --git a/webview-ui/vite.config.ts b/webview-ui/vite.config.ts index 2e4506086a..db401b3306 100644 --- a/webview-ui/vite.config.ts +++ b/webview-ui/vite.config.ts @@ -51,7 +51,7 @@ const persistPortPlugin = (): Plugin => ({ }, }) -// https://vite.dev/config/ +// https://vitejs.dev/config/ export default defineConfig(({ mode }) => { let outDir = "../src/webview-ui/build" @@ -98,7 +98,11 @@ export default defineConfig(({ mode }) => { return { plugins, resolve: { - tsconfigPaths: true, + alias: { + "@": resolve(__dirname, "./src"), + "@src": resolve(__dirname, "./src"), + "@roo": resolve(__dirname, "../src/shared"), + }, }, build: { outDir, @@ -106,32 +110,68 @@ export default defineConfig(({ mode }) => { reportCompressedSize: false, // Generate complete source maps with original TypeScript sources sourcemap: true, - // Vite 8 uses Rolldown/Oxc by default; keep non-production modes readable. - minify: mode === "production", + // Ensure source maps are properly included in the build + minify: mode === "production" ? "esbuild" : false, // Use a single combined CSS bundle so all webviews share styles cssCodeSplit: false, - rolldownOptions: { + rollupOptions: { // Externalize vscode module - it's imported by file-search.ts which is // dynamically imported by roo-config/index.ts, but should never be bundled // in the webview since it's not available in the browser context external: ["vscode"], - input: resolve(__dirname, "index.html"), + input: { + index: resolve(__dirname, "index.html"), + }, output: { - entryFileNames: "assets/[name].js", - chunkFileNames: "assets/[name]-[hash].js", + entryFileNames: `assets/[name].js`, + chunkFileNames: (chunkInfo) => { + if (chunkInfo.name === "mermaid-bundle") { + return `assets/mermaid-bundle.js` + } + // Default naming for other chunks, ensuring uniqueness from entry + return `assets/chunk-[hash].js` + }, assetFileNames: (assetInfo) => { - const name = assetInfo.name ?? "" + const name = assetInfo.name || "" + // Force all CSS into a single predictable file used by both webviews if (name.endsWith(".css")) { return "assets/index.css" } - if (/\.(woff2?|ttf)$/.test(name)) { + if (name.endsWith(".woff2") || name.endsWith(".woff") || name.endsWith(".ttf")) { return "assets/fonts/[name][extname]" } - + // Ensure source maps are included in the build + if (name.endsWith(".map")) { + return "assets/[name]" + } return "assets/[name][extname]" }, + manualChunks: (id, { getModuleInfo }) => { + // Consolidate all mermaid code and its direct large dependencies (like dagre) + // into a single chunk. The 'channel.js' error often points to dagre. + if ( + id.includes("node_modules/mermaid") || + id.includes("node_modules/dagre") || // dagre is a common dep for graph layout + id.includes("node_modules/cytoscape") // another potential graph lib + // Add other known large mermaid dependencies if identified + ) { + return "mermaid-bundle" + } + + // Check if the module is part of any explicitly defined mermaid-related dynamic import + // This is a more advanced check if simple path matching isn't enough. + const moduleInfo = getModuleInfo(id) + if (moduleInfo?.importers.some((importer) => importer.includes("node_modules/mermaid"))) { + return "mermaid-bundle" + } + if ( + moduleInfo?.dynamicImporters.some((importer) => importer.includes("node_modules/mermaid")) + ) { + return "mermaid-bundle" + } + }, }, }, }, @@ -148,6 +188,11 @@ export default defineConfig(({ mode }) => { }, define, optimizeDeps: { + include: [ + "mermaid", + "dagre", // Explicitly include dagre for pre-bundling + // Add other known large mermaid dependencies if identified + ], exclude: ["@vscode/codicons", "vscode-oniguruma", "shiki"], }, assetsInclude: ["**/*.wasm", "**/*.wav"], From 4774842a3c24b344bd7903da4d09189f7ded342d Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 08:00:36 -0600 Subject: [PATCH 09/20] fix(zoo-gateway): dynamic dashboard URL and cached-token fallback - Resolve ModelPicker serviceUrl from zooCodeBaseUrl so staging/dev environments link to the matching dashboard. - Fall back to getCachedZooCodeToken() in the handler and model fetcher when the profile has not been seeded yet (auth before webview open). Co-authored-by: Cursor --- .../fetchers/__tests__/zoo-gateway.spec.ts | 4 ++++ src/api/providers/fetchers/zoo-gateway.ts | 10 ++++++---- src/api/providers/zoo-gateway.ts | 13 +++++++------ .../components/settings/providers/ZooGateway.tsx | 5 ++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts index 8fc2690216..f73ef02b41 100644 --- a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts @@ -5,6 +5,10 @@ import axios from "axios" import { getZooGatewayModels, parseZooGatewayModel } from "../zoo-gateway" vitest.mock("axios") +vitest.mock("../../../../services/zoo-code-auth", () => ({ + getCachedZooCodeToken: vitest.fn(() => ""), + getZooCodeBaseUrl: vitest.fn(() => "https://example.test"), +})) const mockedAxios = axios as any describe("Zoo Gateway Fetchers", () => { diff --git a/src/api/providers/fetchers/zoo-gateway.ts b/src/api/providers/fetchers/zoo-gateway.ts index 8b6922d602..4b9388097a 100644 --- a/src/api/providers/fetchers/zoo-gateway.ts +++ b/src/api/providers/fetchers/zoo-gateway.ts @@ -3,7 +3,7 @@ import axios from "axios" import type { ModelInfo } from "@roo-code/types" import type { ApiHandlerOptions } from "../../../shared/api" -import { getZooCodeBaseUrl } from "../../../services/zoo-code-auth" +import { getCachedZooCodeToken, getZooCodeBaseUrl } from "../../../services/zoo-code-auth" // Reuse the same schemas and parsing logic from vercel-ai-gateway since the API format is identical import { type VercelAiGatewayModel, parseVercelAiGatewayModel } from "./vercel-ai-gateway" @@ -63,10 +63,12 @@ export async function getZooGatewayModels(options?: ApiHandlerOptions): Promise< const models: Record = {} const baseURL = options?.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` - // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token + // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token. + // Fall back to the secret-storage cache when the profile hasn't been seeded yet. + const sessionToken = options?.zooSessionToken || getCachedZooCodeToken() const headers: Record = {} - if (options?.zooSessionToken) { - headers["Authorization"] = `Bearer ${options.zooSessionToken}` + if (sessionToken) { + headers["Authorization"] = `Bearer ${sessionToken}` } try { diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index ab0b632c9a..28c1940033 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -9,7 +9,7 @@ import { } from "@roo-code/types" import { ApiHandlerOptions } from "../../shared/api" -import { getZooCodeBaseUrl } from "../../services/zoo-code-auth" +import { getCachedZooCodeToken, getZooCodeBaseUrl } from "../../services/zoo-code-auth" import { Package } from "../../shared/package" import { ApiStream } from "../transform/stream" @@ -29,10 +29,11 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio constructor(options: ApiHandlerOptions) { const baseURL = options.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` - // Fail fast with a clear message instead of waiting for a 401. - // The token is set automatically by handleZooCodeCallback() after the user - // authenticates via the "Sign in with Zoo Code" flow in the extension. - if (!options.zooSessionToken) { + // Prefer the profile-persisted token; fall back to the secret-storage cache so + // requests work when the user is signed in but the profile hasn't been seeded yet + // (e.g. auth callback arrived before any webview instance was open). + const sessionToken = options.zooSessionToken || getCachedZooCodeToken() + if (!sessionToken) { throw new Error("Zoo Gateway requires authentication. Please sign in to Zoo Code first.") } @@ -51,7 +52,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio }, name: "zoo-gateway", baseURL, - apiKey: options.zooSessionToken, + apiKey: sessionToken, modelId: options.zooGatewayModelId, defaultModelId: zooGatewayDefaultModelId, defaultModelInfo: zooGatewayDefaultModelInfo, diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index bb9e62addd..499a5d20d6 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -33,6 +33,9 @@ export const ZooGateway = ({ useExtensionState() const authUrl = getZooCodeAuthUrl(uriScheme, zooCodeBaseUrl, deviceName) + // Resolve the dashboard link off the same base URL the auth/gateway flow uses, + // so non-prod environments (staging/dev) point at the matching dashboard. + const resolvedDashboardBase = zooCodeBaseUrl?.replace(/\/$/, "") || "https://www.zoocode.dev" return ( <> @@ -73,7 +76,7 @@ export const ZooGateway = ({ models={routerModels?.["zoo-gateway"] ?? {}} modelIdKey="zooGatewayModelId" serviceName="Zoo Gateway" - serviceUrl="https://www.zoocode.dev/dashboard/models" + serviceUrl={`${resolvedDashboardBase}/dashboard/models`} organizationAllowList={organizationAllowList} errorMessage={modelValidationError} simplifySettings={simplifySettings} From 948197fca8a1eab44d576eec8524879eed28a9e5 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 08:11:25 -0600 Subject: [PATCH 10/20] fix(deps): drop stale webview-ui package.json drift that broke frozen-lockfile installs Co-authored-by: Cursor --- webview-ui/package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webview-ui/package.json b/webview-ui/package.json index 7d4baa0204..047ac73ad2 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -48,7 +48,6 @@ "fzf": "^0.5.2", "hast-util-to-jsx-runtime": "^2.3.6", "i18next": "^25.0.0", - "i18next-http-backend": "^3.0.2", "katex": "^0.16.11", "knuth-shuffle-seeded": "^1.0.6", "lru-cache": "^11.1.0", @@ -94,19 +93,19 @@ "@types/diff": "^5.2.1", "@types/jest": "^29.0.0", "@types/katex": "^0.16.7", - "@types/node": "20.x", + "@types/node": "^20.19.25", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.5", "@types/shell-quote": "^1.7.5", "@types/stacktrace-js": "^2.0.3", "@types/vscode-webview": "^1.57.5", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "@vitest/coverage-v8": "^3.2.3", "@vitest/ui": "^3.2.3", "babel-plugin-react-compiler": "^1.0.0", "identity-obj-proxy": "^3.0.0", "jsdom": "^26.0.0", - "vite": "6.3.6", + "vite": "^8.0.13", "vitest": "^3.2.3" } } From ac55c079a3666d94caf128d8ab56bc5293a97132 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 09:09:15 -0600 Subject: [PATCH 11/20] fix(i18n): restore googleCloudCredentialsPathWarning and automaticFetch 'free' search hint dropped on rebase Co-authored-by: Cursor --- webview-ui/src/i18n/locales/ca/settings.json | 3 ++- webview-ui/src/i18n/locales/de/settings.json | 3 ++- webview-ui/src/i18n/locales/en/settings.json | 3 ++- webview-ui/src/i18n/locales/es/settings.json | 3 ++- webview-ui/src/i18n/locales/fr/settings.json | 3 ++- webview-ui/src/i18n/locales/hi/settings.json | 3 ++- webview-ui/src/i18n/locales/id/settings.json | 3 ++- webview-ui/src/i18n/locales/it/settings.json | 3 ++- webview-ui/src/i18n/locales/ja/settings.json | 3 ++- webview-ui/src/i18n/locales/ko/settings.json | 3 ++- webview-ui/src/i18n/locales/nl/settings.json | 3 ++- webview-ui/src/i18n/locales/pl/settings.json | 3 ++- webview-ui/src/i18n/locales/pt-BR/settings.json | 3 ++- webview-ui/src/i18n/locales/ru/settings.json | 3 ++- webview-ui/src/i18n/locales/tr/settings.json | 3 ++- webview-ui/src/i18n/locales/vi/settings.json | 3 ++- webview-ui/src/i18n/locales/zh-CN/settings.json | 3 ++- webview-ui/src/i18n/locales/zh-TW/settings.json | 3 ++- 18 files changed, 36 insertions(+), 18 deletions(-) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 316e3f66ac..db0ca3317e 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -452,6 +452,7 @@ "step3": "3. O crear un compte de servei amb credencials." }, "googleCloudCredentials": "Credencials de Google Cloud", + "googleCloudCredentialsPathWarning": "Aquest camp espera el contingut JSON d'un fitxer de clau de compte de servei, no una ruta. Si tens una ruta, enganxa-la al camp Ruta del fitxer de clau de Google Cloud a continuaci├│, o buida aquest camp i utilitza la variable d'entorn GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Ruta del fitxer de clau de Google Cloud", "googleCloudProjectId": "ID del projecte de Google Cloud", "googleCloudRegion": "Regió de Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "L'extensió obté automàticament la llista més recent de models disponibles a {{serviceName}}. Si no esteu segur de quin model triar, Zoo Code funciona millor amb {{defaultModelId}}.", + "automaticFetch": "L'extensi├│ obt├⌐ autom├áticament la llista m├⌐s recent de models disponibles a {{serviceName}}. Si no esteu segur de quin model triar, Zoo Code funciona millor amb {{defaultModelId}}. Tamb├⌐ podeu cercar \"free\" per a opcions gratu├»tes actualment disponibles.", "label": "Model", "searchPlaceholder": "Cerca", "noMatchFound": "No s'ha trobat cap coincidència", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 7fbb2021d6..716f44d556 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -452,6 +452,7 @@ "step3": "3. Oder ein Servicekonto mit Anmeldeinformationen erstellen." }, "googleCloudCredentials": "Google Cloud Anmeldedaten", + "googleCloudCredentialsPathWarning": "Dieses Feld erwartet den JSON-Inhalt einer Dienstkonto-Schl├╝sseldatei, keinen Pfad. Wenn Sie einen Pfad haben, f├╝gen Sie ihn unten in das Feld Google Cloud Schl├╝sseldateipfad ein, oder leeren Sie dieses Feld und verwenden Sie die Umgebungsvariable GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Google Cloud Schlüsseldateipfad", "googleCloudProjectId": "Google Cloud Projekt-ID", "googleCloudRegion": "Google Cloud Region", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "Die Erweiterung ruft automatisch die neueste Liste der auf {{serviceName}} verfügbaren Modelle ab. Wenn du dir nicht sicher bist, welches Modell du wählen sollst, funktioniert Zoo Code am besten mit {{defaultModelId}}.", + "automaticFetch": "Die Erweiterung ruft automatisch die neueste Liste der auf {{serviceName}} verf├╝gbaren Modelle ab. Wenn du dir nicht sicher bist, welches Modell du w├ñhlen sollst, funktioniert Zoo Code am besten mit {{defaultModelId}}. Du kannst auch versuchen, nach \"kostenlos\" zu suchen, um die derzeit verf├╝gbaren kostenlosen Optionen zu finden.", "label": "Modell", "searchPlaceholder": "Suchen", "noMatchFound": "Keine Übereinstimmung gefunden", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c0591680bd..98aa51632d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -515,6 +515,7 @@ "step3": "3. Or create a service account with credentials." }, "googleCloudCredentials": "Google Cloud Credentials", + "googleCloudCredentialsPathWarning": "This field expects the JSON contents of a service-account key file, not a path. If you have a path, paste it into the Google Cloud Key File Path field below, or clear this field and use the GOOGLE_APPLICATION_CREDENTIALS environment variable.", "googleCloudKeyFile": "Google Cloud Key File Path", "googleCloudProjectId": "Google Cloud Project ID", "googleCloudRegion": "Google Cloud Region", @@ -930,7 +931,7 @@ } }, "modelPicker": { - "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Zoo Code works best with {{defaultModelId}}.", + "automaticFetch": "The extension automatically fetches the latest list of models available on {{serviceName}}. If you're unsure which model to choose, Zoo Code works best with {{defaultModelId}}. You can also try searching \"free\" for no-cost options currently available.", "label": "Model", "searchPlaceholder": "Search", "noMatchFound": "No match found", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 06f4fc3093..dc87fb7d4f 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -452,6 +452,7 @@ "step3": "3. O crear una cuenta de servicio con credenciales." }, "googleCloudCredentials": "Credenciales de Google Cloud", + "googleCloudCredentialsPathWarning": "Este campo espera el contenido JSON de un archivo de clave de cuenta de servicio, no una ruta. Si tienes una ruta, p├⌐gala en el campo Ruta del archivo de clave de Google Cloud a continuaci├│n, o borra este campo y utiliza la variable de entorno GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Ruta del archivo de clave de Google Cloud", "googleCloudProjectId": "ID del proyecto de Google Cloud", "googleCloudRegion": "Región de Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "La extensión obtiene automáticamente la lista más reciente de modelos disponibles en {{serviceName}}. Si no está seguro de qué modelo elegir, Zoo Code funciona mejor con {{defaultModelId}}.", + "automaticFetch": "La extensi├│n obtiene autom├íticamente la lista m├ís reciente de modelos disponibles en {{serviceName}}. Si no est├í seguro de qu├⌐ modelo elegir, Zoo Code funciona mejor con {{defaultModelId}}. Tambi├⌐n puede buscar \"free\" para opciones sin costo actualmente disponibles.", "label": "Modelo", "searchPlaceholder": "Buscar", "noMatchFound": "No se encontraron coincidencias", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 1f98533162..eee7bc21ca 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -452,6 +452,7 @@ "step3": "3. Ou créer un compte de service avec des identifiants." }, "googleCloudCredentials": "Identifiants Google Cloud", + "googleCloudCredentialsPathWarning": "Ce champ attend le contenu JSON d'un fichier de cl├⌐ de compte de service, pas un chemin. Si vous disposez d'un chemin, collez-le dans le champ Chemin du fichier de cl├⌐ Google Cloud ci-dessous, ou videz ce champ et utilisez la variable d'environnement GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Chemin du fichier de clé Google Cloud", "googleCloudProjectId": "ID du projet Google Cloud", "googleCloudRegion": "Région Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "L'extension récupère automatiquement la liste la plus récente des modèles disponibles sur {{serviceName}}. Si vous ne savez pas quel modèle choisir, Zoo Code fonctionne mieux avec {{defaultModelId}}.", + "automaticFetch": "L'extension r├⌐cup├¿re automatiquement la liste la plus r├⌐cente des mod├¿les disponibles sur {{serviceName}}. Si vous ne savez pas quel mod├¿le choisir, Zoo Code fonctionne mieux avec {{defaultModelId}}. Vous pouvez ├⌐galement rechercher \"free\" pour les options gratuites actuellement disponibles.", "label": "Modèle", "searchPlaceholder": "Rechercher", "noMatchFound": "Aucune correspondance trouvée", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index b06fa07c94..da4a5f7a54 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -452,6 +452,7 @@ "step3": "3. या क्रेडेंशियल्स के साथ एक सर्विस अकाउंट बनाएं।" }, "googleCloudCredentials": "Google Cloud क्रेडेंशियल्स", + "googleCloudCredentialsPathWarning": "αñçαñ╕ αñ½αñ╝αÑÇαñ▓αÑìαñí αñ«αÑçαñé αñ╕αñ░αÑìαñ╡αñ┐αñ╕-αñàαñòαñ╛αñëαñéαñƒ αñòαÑüαñéαñ£αÑÇ αñ½αñ╝αñ╛αñçαñ▓ αñòαÑÇ JSON αñ╕αñ╛αñ«αñùαÑìαñ░αÑÇ αñàαñ¬αÑçαñòαÑìαñ╖αñ┐αññ αñ╣αÑê, αñ¬αñÑ αñ¿αñ╣αÑÇαñéαÑñ αñ»αñªαñ┐ αñåαñ¬αñòαÑç αñ¬αñ╛αñ╕ αñ¬αñÑ αñ╣αÑê, αññαÑï αñëαñ╕αÑç αñ¿αÑÇαñÜαÑç αñªαñ┐αñÅ αñùαñÅ Google Cloud αñòαÑüαñéαñ£αÑÇ αñ½αñ╝αñ╛αñçαñ▓ αñ¬αñÑ αñ½αñ╝αÑÇαñ▓αÑìαñí αñ«αÑçαñé αñ¬αÑçαñ╕αÑìαñƒ αñòαñ░αÑçαñé, αñ»αñ╛ αñçαñ╕ αñ½αñ╝αÑÇαñ▓αÑìαñí αñòαÑï αñûαñ╛αñ▓αÑÇ αñòαñ░αÑçαñé αñöαñ░ GOOGLE_APPLICATION_CREDENTIALS αñ¬αñ░αÑìαñ»αñ╛αñ╡αñ░αñú αñÜαñ░ αñòαñ╛ αñëαñ¬αñ»αÑïαñù αñòαñ░αÑçαñéαÑñ", "googleCloudKeyFile": "Google Cloud कुंजी फ़ाइल पथ", "googleCloudProjectId": "Google Cloud प्रोजेक्ट ID", "googleCloudRegion": "Google Cloud क्षेत्र", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है।", + "automaticFetch": "αñÅαñòαÑìαñ╕αñƒαÑçαñéαñ╢αñ¿ {{serviceName}} αñ¬αñ░ αñëαñ¬αñ▓αñ¼αÑìαñº αñ«αÑëαñíαñ▓αÑïαñé αñòαÑÇ αñ¿αñ╡αÑÇαñ¿αññαñ« αñ╕αÑéαñÜαÑÇ αñ╕αÑìαñ╡αñÜαñ╛αñ▓αñ┐αññ αñ░αÑéαñ¬ αñ╕αÑç αñ¬αÑìαñ░αñ╛αñ¬αÑìαññ αñòαñ░αññαñ╛ αñ╣αÑêαÑñ αñ»αñªαñ┐ αñåαñ¬ αñàαñ¿αñ┐αñ╢αÑìαñÜαñ┐αññ αñ╣αÑêαñé αñòαñ┐ αñòαÑîαñ¿ αñ╕αñ╛ αñ«αÑëαñíαñ▓ αñÜαÑüαñ¿αñ¿αñ╛ αñ╣αÑê, αññαÑï Zoo Code {{defaultModelId}} αñòαÑç αñ╕αñ╛αñÑ αñ╕αñ¼αñ╕αÑç αñàαñÜαÑìαñ¢αñ╛ αñòαñ╛αñ« αñòαñ░αññαñ╛ αñ╣αÑêαÑñ αñåαñ¬ αñ╡αñ░αÑìαññαñ«αñ╛αñ¿ αñ«αÑçαñé αñëαñ¬αñ▓αñ¼αÑìαñº αñ¿αñ┐αñâαñ╢αÑüαñ▓αÑìαñò αñ╡αñ┐αñòαñ▓αÑìαñ¬αÑïαñé αñòαÑç αñ▓αñ┐αñÅ \"free\" αñ¡αÑÇ αñûαÑïαñ£ αñ╕αñòαññαÑç αñ╣αÑêαñéαÑñ", "label": "मॉडल", "searchPlaceholder": "खोजें", "noMatchFound": "कोई मिलान नहीं मिला", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index bc30c6e414..f9751f57d2 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -452,6 +452,7 @@ "step3": "3. Atau buat service account dengan credentials." }, "googleCloudCredentials": "Google Cloud Credentials", + "googleCloudCredentialsPathWarning": "Bidang ini mengharapkan konten JSON dari file kunci akun layanan, bukan path. Jika Anda memiliki path, tempelkan ke bidang Path File Key Google Cloud di bawah, atau kosongkan bidang ini dan gunakan variabel lingkungan GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Path File Key Google Cloud", "googleCloudProjectId": "Google Cloud Project ID", "googleCloudRegion": "Google Cloud Region", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "Ekstensi secara otomatis mengambil daftar model terbaru yang tersedia di {{serviceName}}. Jika kamu tidak yakin model mana yang harus dipilih, Zoo Code bekerja terbaik dengan {{defaultModelId}}.", + "automaticFetch": "Ekstensi secara otomatis mengambil daftar model terbaru yang tersedia di {{serviceName}}. Jika kamu tidak yakin model mana yang harus dipilih, Zoo Code bekerja terbaik dengan {{defaultModelId}}. Kamu juga dapat mencoba mencari \"free\" untuk opsi tanpa biaya yang saat ini tersedia.", "label": "Model", "searchPlaceholder": "Cari", "noMatchFound": "Tidak ada yang cocok ditemukan", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 39daaedb69..e4496988c9 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -452,6 +452,7 @@ "step3": "3. Oppure creare un account di servizio con credenziali." }, "googleCloudCredentials": "Credenziali Google Cloud", + "googleCloudCredentialsPathWarning": "Questo campo si aspetta il contenuto JSON di un file di chiave di account di servizio, non un percorso. Se hai un percorso, incollalo nel campo Percorso file chiave Google Cloud qui sotto, oppure svuota questo campo e usa la variabile d'ambiente GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Percorso file chiave Google Cloud", "googleCloudProjectId": "ID progetto Google Cloud", "googleCloudRegion": "Regione Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "L'estensione recupera automaticamente l'elenco più recente dei modelli disponibili su {{serviceName}}. Se non sei sicuro di quale modello scegliere, Zoo Code funziona meglio con {{defaultModelId}}.", + "automaticFetch": "L'estensione recupera automaticamente l'elenco pi├╣ recente dei modelli disponibili su {{serviceName}}. Se non sei sicuro di quale modello scegliere, Zoo Code funziona meglio con {{defaultModelId}}. Puoi anche cercare \"free\" per opzioni gratuite attualmente disponibili.", "label": "Modello", "searchPlaceholder": "Cerca", "noMatchFound": "Nessuna corrispondenza trovata", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 656f9846c4..d8bf7418c4 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -452,6 +452,7 @@ "step3": "3. または、認証情報付きのサービスアカウントを作成します。" }, "googleCloudCredentials": "Google Cloud認証情報", + "googleCloudCredentialsPathWarning": "πüôπü«πâòπéúπâ╝πâ½πâëπü»πÇüπâæπé╣πüºπü»πü¬πüÅπÇüπé╡πâ╝πâôπé╣πéóπé½πéªπâ│πâêπé¡πâ╝πâòπéíπéñπâ½πü«JSONσåàσ«╣π鯵£ƒσ╛àπüùπü╛πüÖπÇéπâæπé╣πéÆπüèµîüπüíπü«σá┤σÉêπü»πÇüΣ╕ïπü«Google Cloudπé¡πâ╝πâòπéíπéñπâ½πâæπé╣πâòπéúπâ╝πâ½πâëπü½Φ▓╝πéèΣ╗ÿπüæπéïπüïπÇüπüôπü«πâòπéúπâ╝πâ½πâëπéÆπé»πâ¬πéóπüùπüªGOOGLE_APPLICATION_CREDENTIALSτÆ░σóâσñëµò░πéÆΣ╜┐τö¿πüùπüªπüÅπüáπüòπüäπÇé", "googleCloudKeyFile": "Google Cloudキーファイルパス", "googleCloudProjectId": "Google Cloudプロジェクトid", "googleCloudRegion": "Google Cloudリージョン", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。", + "automaticFetch": "µïíσ╝╡µ⌐ƒΦâ╜πü»{{serviceName}}πüºσê⌐τö¿σÅ»Φâ╜πü¬µ£Çµû░πü«πâóπâçπâ½πâ¬πé╣πâêπéÆΦç¬σïòτÜäπü½σÅûσ╛ùπüùπü╛πüÖπÇéπü⌐πü«πâóπâçπâ½πéÆΘü╕πü╢πü╣πüìπüïΦ┐╖πüúπüªπüäπéïσá┤σÉêπÇüZoo Codeπü»{{defaultModelId}}πüºµ£ÇΘü⌐πü½σïòΣ╜£πüùπü╛πüÖπÇéπü╛πüƒπÇüπÇîfreeπÇìπüºµñ£τ┤óπüÖπéïπü¿πÇüτÅ╛σ£¿σê⌐τö¿σÅ»Φâ╜πü¬τäíµûÖπé¬πâùπé╖πâºπâ│πéÆΦªïπüñπüæπéïπüôπü¿πüîπüºπüìπü╛πüÖπÇé", "label": "モデル", "searchPlaceholder": "検索", "noMatchFound": "一致するものが見つかりません", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 3e054d28d0..d35c0136a1 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -452,6 +452,7 @@ "step3": "3. 또는 자격 증명이 있는 서비스 계정을 만드세요." }, "googleCloudCredentials": "Google Cloud 자격 증명", + "googleCloudCredentialsPathWarning": "∞¥┤ φòäδô£δèö Ω▓╜δí£Ω░Ç ∞òäδïî ∞ä£δ╣ä∞èñ Ω│ä∞áò φéñ φîî∞¥╝∞¥ÿ JSON δé┤∞Ü⌐∞¥ä Ω╕░δîÇφò⌐δïêδïñ. Ω▓╜δí£Ω░Ç ∞₧êδïñδ⌐┤ ∞òäδ₧ÿ∞¥ÿ Google Cloud φéñ φîî∞¥╝ Ω▓╜δí£ φòäδô£∞ùÉ δ╢Ö∞ù¼δäúΩ▒░δéÿ, ∞¥┤ φòäδô£δÑ╝ δ╣ä∞Ü░Ω│á GOOGLE_APPLICATION_CREDENTIALS φÖÿΩ▓╜ δ│Ç∞êÿδÑ╝ ∞é¼∞Ü⌐φòÿ∞ä╕∞Üö.", "googleCloudKeyFile": "Google Cloud 키 파일 경로", "googleCloudProjectId": "Google Cloud 프로젝트 ID", "googleCloudRegion": "Google Cloud 리전", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다.", + "automaticFetch": "φÖò∞₧Ñ φöäδí£Ω╖╕δ₧¿∞¥Ç {{serviceName}}∞ùÉ∞ä£ ∞é¼∞Ü⌐ Ω░ÇδèÑφò£ ∞╡£∞ïá 모δì╕ δ¬⌐δí¥∞¥ä ∞₧ÉδÅÖ∞£╝δí£ Ω░Ç∞á╕∞ÿ╡δïêδïñ. ∞û┤δûñ 모δì╕∞¥ä ∞äáφâ¥φò┤∞ò╝ φòá∞ºÇ φÖò∞ïñφòÿ∞ºÇ ∞òèδïñδ⌐┤, Zoo Codeδèö {{defaultModelId}}δí£ Ω░Ç∞₧Ñ ∞₧ÿ ∞₧æδÅÖφò⌐δïêδïñ. φÿä∞₧¼ ∞é¼∞Ü⌐ Ω░ÇδèÑφò£ δ¼┤δúî ∞ÿ╡∞àÿ∞¥ä ∞░╛∞£╝δáñδ⌐┤ \"free\"δÑ╝ Ω▓Ç∞âëφò┤ δ│╝ ∞êÿδÅä ∞₧ê∞è╡δïêδïñ.", "label": "모델", "searchPlaceholder": "검색", "noMatchFound": "일치하는 항목 없음", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 0744ef9d32..c944a54245 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -452,6 +452,7 @@ "step3": "3. Of maak een serviceaccount met referenties." }, "googleCloudCredentials": "Google Cloud-referenties", + "googleCloudCredentialsPathWarning": "Dit veld verwacht de JSON-inhoud van een serviceaccount-sleutelbestand, geen pad. Als je een pad hebt, plak het dan in het veld Google Cloud-sleutelbestandspad hieronder, of leeg dit veld en gebruik de omgevingsvariabele GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Google Cloud-sleutelbestandspad", "googleCloudProjectId": "Google Cloud-project-ID", "googleCloudRegion": "Google Cloud-regio", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "De extensie haalt automatisch de nieuwste lijst met modellen op van {{serviceName}}. Weet je niet welk model je moet kiezen? Zoo Code werkt het beste met {{defaultModelId}}.", + "automaticFetch": "De extensie haalt automatisch de nieuwste lijst met modellen op van {{serviceName}}. Weet je niet welk model je moet kiezen? Zoo Code werkt het beste met {{defaultModelId}}. Je kunt ook zoeken op 'free' voor gratis opties die nu beschikbaar zijn.", "label": "Model", "searchPlaceholder": "Zoeken", "noMatchFound": "Geen overeenkomsten gevonden", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 71fc221944..07fce228ef 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -452,6 +452,7 @@ "step3": "3. Lub utworzyć konto usługi z poświadczeniami." }, "googleCloudCredentials": "Poświadczenia Google Cloud", + "googleCloudCredentialsPathWarning": "To pole oczekuje zawarto┼¢ci JSON pliku klucza konta us┼éugi, a nie ┼¢cie┼╝ki. Je┼¢li masz ┼¢cie┼╝k─Ö, wklej j─à do pola ┼Ücie┼╝ka pliku klucza Google Cloud poni┼╝ej lub wyczy┼¢─ç to pole i u┼╝yj zmiennej ┼¢rodowiskowej GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Ścieżka pliku klucza Google Cloud", "googleCloudProjectId": "ID projektu Google Cloud", "googleCloudRegion": "Region Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "Rozszerzenie automatycznie pobiera najnowszą listę modeli dostępnych w {{serviceName}}. Jeśli nie jesteś pewien, który model wybrać, Zoo Code działa najlepiej z {{defaultModelId}}.", + "automaticFetch": "Rozszerzenie automatycznie pobiera najnowsz─à list─Ö modeli dost─Öpnych w {{serviceName}}. Je┼¢li nie jeste┼¢ pewien, kt├│ry model wybra─ç, Zoo Code dzia┼éa najlepiej z {{defaultModelId}}. Mo┼╝esz r├│wnie┼╝ wyszuka─ç \"free\", aby znale┼║─ç obecnie dost─Öpne opcje bezp┼éatne.", "label": "Model", "searchPlaceholder": "Wyszukaj", "noMatchFound": "Nie znaleziono dopasowań", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index fd2f065cd4..48ed1e53b1 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -452,6 +452,7 @@ "step3": "3. Ou criar uma conta de serviço com credenciais." }, "googleCloudCredentials": "Credenciais Google Cloud", + "googleCloudCredentialsPathWarning": "Este campo espera o conte├║do JSON de um arquivo de chave de conta de servi├ºo, n├úo um caminho. Se voc├¬ tiver um caminho, cole-o no campo Caminho do Arquivo de Chave Google Cloud abaixo, ou limpe este campo e use a vari├ível de ambiente GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Caminho do Arquivo de Chave Google Cloud", "googleCloudProjectId": "ID do Projeto Google Cloud", "googleCloudRegion": "Região Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "A extensão busca automaticamente a lista mais recente de modelos disponíveis em {{serviceName}}. Se você não tem certeza sobre qual modelo escolher, o Zoo Code funciona melhor com {{defaultModelId}}.", + "automaticFetch": "A extens├úo busca automaticamente a lista mais recente de modelos dispon├¡veis em {{serviceName}}. Se voc├¬ n├úo tem certeza sobre qual modelo escolher, o Zoo Code funciona melhor com {{defaultModelId}}. Voc├¬ tamb├⌐m pode pesquisar por \"free\" para encontrar op├º├╡es gratuitas atualmente dispon├¡veis.", "label": "Modelo", "searchPlaceholder": "Pesquisar", "noMatchFound": "Nenhuma correspondência encontrada", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 3772f8c749..ce55e97fca 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -452,6 +452,7 @@ "step3": "3. Или создайте сервисный аккаунт с ключом." }, "googleCloudCredentials": "Учётные данные Google Cloud", + "googleCloudCredentialsPathWarning": "╨¡╤é╨╛ ╨┐╨╛╨╗╨╡ ╨╛╨╢╨╕╨┤╨░╨╡╤é JSON-╤ü╨╛╨┤╨╡╤Ç╨╢╨╕╨╝╨╛╨╡ ╤ä╨░╨╣╨╗╨░ ╨║╨╗╤Ä╤ç╨░ ╤ü╨╡╤Ç╨▓╨╕╤ü╨╜╨╛╨│╨╛ ╨░╨║╨║╨░╤â╨╜╤é╨░, ╨░ ╨╜╨╡ ╨┐╤â╤é╤î. ╨ò╤ü╨╗╨╕ ╤â ╨▓╨░╤ü ╨╡╤ü╤é╤î ╨┐╤â╤é╤î, ╨▓╤ü╤é╨░╨▓╤î╤é╨╡ ╨╡╨│╨╛ ╨▓ ╨┐╨╛╨╗╨╡ ╨ƒ╤â╤é╤î ╨║ ╨║╨╗╤Ä╤ç╤â Google Cloud ╨╜╨╕╨╢╨╡, ╨╗╨╕╨▒╨╛ ╨╛╤ç╨╕╤ü╤é╨╕╤é╨╡ ╤ì╤é╨╛ ╨┐╨╛╨╗╨╡ ╨╕ ╨╕╤ü╨┐╨╛╨╗╤î╨╖╤â╨╣╤é╨╡ ╨┐╨╡╤Ç╨╡╨╝╨╡╨╜╨╜╤â╤Ä ╨╛╨║╤Ç╤â╨╢╨╡╨╜╨╕╤Å GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Путь к ключу Google Cloud", "googleCloudProjectId": "ID проекта Google Cloud", "googleCloudRegion": "Регион Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}.", + "automaticFetch": "╨á╨░╤ü╤ê╨╕╤Ç╨╡╨╜╨╕╨╡ ╨░╨▓╤é╨╛╨╝╨░╤é╨╕╤ç╨╡╤ü╨║╨╕ ╨┐╨╛╨╗╤â╤ç╨░╨╡╤é ╨░╨║╤é╤â╨░╨╗╤î╨╜╤ï╨╣ ╤ü╨┐╨╕╤ü╨╛╨║ ╨╝╨╛╨┤╨╡╨╗╨╡╨╣ ╨╜╨░ {{serviceName}}. ╨ò╤ü╨╗╨╕ ╨╜╨╡ ╤â╨▓╨╡╤Ç╨╡╨╜╤ï, ╤ç╤é╨╛ ╨▓╤ï╨▒╤Ç╨░╤é╤î, Zoo Code ╨╗╤â╤ç╤ê╨╡ ╨▓╤ü╨╡╨│╨╛ ╤Ç╨░╨▒╨╛╤é╨░╨╡╤é ╤ü {{defaultModelId}}. ╨ó╨░╨║╨╢╨╡ ╨┐╨╛╨┐╤Ç╨╛╨▒╤â╨╣╤é╨╡ ╨┐╨╛╨╕╤ü╨║╨░╤é╤î \"free\" ╨┤╨╗╤Å ╨▒╨╡╤ü╨┐╨╗╨░╤é╨╜╤ï╤à ╨▓╨░╤Ç╨╕╨░╨╜╤é╨╛╨▓.", "label": "Модель", "searchPlaceholder": "Поиск", "noMatchFound": "Совпадений не найдено", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index d912f9efbd..9c6a62953e 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -452,6 +452,7 @@ "step3": "3. Veya kimlik bilgileriyle bir hizmet hesabı oluşturun." }, "googleCloudCredentials": "Google Cloud Kimlik Bilgileri", + "googleCloudCredentialsPathWarning": "Bu alan, bir yol de─ƒil, bir hizmet hesab─▒ anahtar dosyas─▒n─▒n JSON i├ºeri─ƒini bekler. Bir yolunuz varsa, a┼ƒa─ƒ─▒daki Google Cloud Anahtar Dosyas─▒ Yolu alan─▒na yap─▒┼ƒt─▒r─▒n veya bu alan─▒ temizleyin ve GOOGLE_APPLICATION_CREDENTIALS ortam de─ƒi┼ƒkenini kullan─▒n.", "googleCloudKeyFile": "Google Cloud Anahtar Dosyası Yolu", "googleCloudProjectId": "Google Cloud Proje Kimliği", "googleCloudRegion": "Google Cloud Bölgesi", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "Uzantı {{serviceName}} üzerinde bulunan mevcut modellerin en güncel listesini otomatik olarak alır. Hangi modeli seçeceğinizden emin değilseniz, Zoo Code {{defaultModelId}} ile en iyi şekilde çalışır.", + "automaticFetch": "Uzant─▒ {{serviceName}} ├╝zerinde bulunan mevcut modellerin en g├╝ncel listesini otomatik olarak al─▒r. Hangi modeli se├ºece─ƒinizden emin de─ƒilseniz, Zoo Code {{defaultModelId}} ile en iyi ┼ƒekilde ├ºal─▒┼ƒ─▒r. ┼₧u anda mevcut olan ├╝cretsiz se├ºenekleri bulmak i├ºin \"free\" aramas─▒ da yapabilirsiniz.", "label": "Model", "searchPlaceholder": "Ara", "noMatchFound": "Eşleşme bulunamadı", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index c383443bbf..cf033c24b7 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -452,6 +452,7 @@ "step3": "3. Hoặc tạo tài khoản dịch vụ với thông tin xác thực." }, "googleCloudCredentials": "Thông tin xác thực Google Cloud", + "googleCloudCredentialsPathWarning": "Tr╞░ß╗¥ng n├áy mong ─æß╗úi nß╗Öi dung JSON cß╗ºa tß╗çp kh├│a t├ái khoß║ún dß╗ïch vß╗Ñ, kh├┤ng phß║úi ─æ╞░ß╗¥ng dß║½n. Nß║┐u bß║ín c├│ ─æ╞░ß╗¥ng dß║½n, h├úy d├ín v├áo tr╞░ß╗¥ng ─É╞░ß╗¥ng dß║½n tß╗çp kh├│a Google Cloud b├¬n d╞░ß╗¢i, hoß║╖c x├│a tr╞░ß╗¥ng n├áy v├á sß╗¡ dß╗Ñng biß║┐n m├┤i tr╞░ß╗¥ng GOOGLE_APPLICATION_CREDENTIALS.", "googleCloudKeyFile": "Đường dẫn tệp khóa Google Cloud", "googleCloudProjectId": "ID dự án Google Cloud", "googleCloudRegion": "Vùng Google Cloud", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "Tiện ích mở rộng tự động lấy danh sách mới nhất các mô hình có sẵn trên {{serviceName}}. Nếu bạn không chắc chắn nên chọn mô hình nào, Zoo Code hoạt động tốt nhất với {{defaultModelId}}.", + "automaticFetch": "Tiß╗çn ├¡ch mß╗ƒ rß╗Öng tß╗▒ ─æß╗Öng lß║Ñy danh s├ích mß╗¢i nhß║Ñt c├íc m├┤ h├¼nh c├│ sß║╡n tr├¬n {{serviceName}}. Nß║┐u bß║ín kh├┤ng chß║»c chß║»n n├¬n chß╗ìn m├┤ h├¼nh n├áo, Zoo Code hoß║ít ─æß╗Öng tß╗æt nhß║Ñt vß╗¢i {{defaultModelId}}. Bß║ín c┼⌐ng c├│ thß╗â thß╗¡ t├¼m kiß║┐m \"free\" cho c├íc t├╣y chß╗ìn miß╗àn ph├¡ hiß╗çn c├│.", "label": "Mô hình", "searchPlaceholder": "Tìm kiếm", "noMatchFound": "Không tìm thấy kết quả", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 9f5e32226f..bd3a38cdba 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -452,6 +452,7 @@ "step3": "3. 创建服务账号获取凭证" }, "googleCloudCredentials": "Google Cloud 凭证", + "googleCloudCredentialsPathWarning": "µ¡ñσ¡ùµ«╡Θ£ÇΦªüµ£ìσèíΦ┤ªσÅ╖σ»åΘÆÑµûçΣ╗╢τÜä JSON σåàσ«╣∩╝îΦÇîΣ╕ìµÿ»Φ╖»σ╛äπÇéσªéµ₧£µé¿µ£ëΦ╖»σ╛ä∩╝îΦ»╖σ░åσà╢τ▓ÿΦ┤┤σê░Σ╕ïµû╣τÜäGoogle Cloud σ»åΘÆÑµûçΣ╗╢Φ╖»σ╛äσ¡ùµ«╡Σ╕¡∩╝îµêûµ╕àΘÖñµ¡ñσ¡ùµ«╡σ╣╢Σ╜┐τö¿ GOOGLE_APPLICATION_CREDENTIALS τÄ»σóâσÅÿΘçÅπÇé", "googleCloudKeyFile": "Google Cloud 密钥文件路径", "googleCloudProjectId": "Google Cloud 项目 ID", "googleCloudRegion": "Google Cloud 区域", @@ -867,7 +868,7 @@ } }, "modelPicker": { - "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。", + "automaticFetch": "Φç¬σè¿ΦÄ╖σÅû {{serviceName}} Σ╕èσÅ»τö¿τÜ䵣ǵû░µ¿íσ₧ïσêùΦí¿πÇéσªéµ₧£µé¿Σ╕ìτí«σ«ÜΘÇëµï⌐σô¬Σ╕¬µ¿íσ₧ï∩╝îZoo Code Σ╕Ä {{defaultModelId}} ΘàìσÉêµ£ÇΣ╜│πÇéµé¿Φ┐ÿσÅ»Σ╗ѵɣτ┤ó\"free\"Σ╗ѵƒÑµë╛σ╜ôσëìσÅ»τö¿τÜäσàìΦ┤╣ΘÇëΘí╣πÇé", "label": "模型", "searchPlaceholder": "搜索", "noMatchFound": "未找到匹配项", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 309ed06bd1..096e4312ad 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -462,6 +462,7 @@ "step3": "3. 或建立具有憑證的服務帳戶。" }, "googleCloudCredentials": "Google Cloud 憑證", + "googleCloudCredentialsPathWarning": "µ¡ñµ¼äΣ╜ìΘ£ÇΦªüµ£ìσïÖσ╕│ΦÖƒΘçæΘæ░µ¬öµíêτÜä JSON σàºσ«╣∩╝îΦÇîΣ╕ìµÿ»Φ╖»σ╛æπÇéσªéµ₧£µé¿µ£ëΦ╖»σ╛æ∩╝îΦ½ïσ░çσà╢Φ▓╝σê░Σ╕ïµû╣τÜäGoogle Cloud ΘçæΘæ░µ¬öµíêΦ╖»σ╛æµ¼äΣ╜ì∩╝îµêûµ╕àΘÖñµ¡ñµ¼äΣ╜ìΣ╕ªΣ╜┐τö¿ GOOGLE_APPLICATION_CREDENTIALS τÆ░σóâΦ«èµò╕πÇé", "googleCloudKeyFile": "Google Cloud 金鑰檔案路徑", "googleCloudProjectId": "Google Cloud 專案 ID", "googleCloudRegion": "Google Cloud 區域", @@ -877,7 +878,7 @@ } }, "modelPicker": { - "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。", + "automaticFetch": "µ¡ñµô┤σààσèƒΦâ╜µ£âΦç¬σïòσ╛₧ {{serviceName}} σÅûσ╛ùµ£Çµû░τÜäσÅ»τö¿µ¿íσ₧ïµ╕àσû«πÇéσªéµ₧£Σ╕ìτó║σ«ÜΦªüΘü╕σô¬σÇﵿíσ₧ï∩╝îσ╗║Φ¡░Σ╜┐τö¿ {{defaultModelId}}∩╝îΘÇÖµÿ»Φêç Zoo Code µ£ÇΣ╜│µÉ¡ΘàìτÜ䵿íσ₧ïπÇéµé¿Σ╣ƒσÅ»Σ╗ѵɣσ░ïπÇîfreeπÇìΣ╛嵬óΦªûτ¢«σëìσÅ»τö¿τÜäσàìΦ▓╗Θü╕ΘáàπÇé", "label": "模型", "searchPlaceholder": "搜尋", "noMatchFound": "找不到符合的結果", From 8877ec79364afecfb4989c0237d2d3f23f37bf08 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 28 May 2026 14:23:20 -0600 Subject: [PATCH 12/20] fix(zoo-gateway): pick default model from fetched list, prefer Sonnet 4.5 Resolves Sonnet 4.5 from the gateway model catalog instead of a static Vercel slug so test (Bedrock) and live accounts both get a valid default. Reassigns stale profile model IDs when they are not in the catalog. Co-authored-by: Cursor --- .../settings/providers/ZooGateway.tsx | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index 499a5d20d6..e6a24e45f3 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -1,3 +1,4 @@ +import { useEffect, useMemo } from "react" import { type ProviderSettings, type OrganizationAllowList, @@ -20,6 +21,32 @@ type ZooGatewayProps = { simplifySettings?: boolean } +function isSonnet45ModelId(id: string) { + return /sonnet-4[.-]5|sonnet-4\.5/i.test(id) +} + +function pickZooGatewayDefaultModelId(modelIds: string[]) { + if (modelIds.length === 0) { + return zooGatewayDefaultModelId + } + + const sonnet45 = modelIds.filter(isSonnet45ModelId) + if (sonnet45.length > 0) { + return ( + sonnet45.find((id) => id === "anthropic/claude-sonnet-4.5") ?? + sonnet45.find((id) => id.includes("claude-sonnet-4.5")) ?? + sonnet45[0] + ) + } + + const sonnet4 = modelIds.filter((id) => /claude/i.test(id) && /sonnet/i.test(id) && /sonnet-4/i.test(id)) + if (sonnet4.length > 0) { + return sonnet4[0] + } + + return modelIds[0] +} + export const ZooGateway = ({ apiConfiguration, setApiConfigurationField, @@ -33,13 +60,25 @@ export const ZooGateway = ({ useExtensionState() const authUrl = getZooCodeAuthUrl(uriScheme, zooCodeBaseUrl, deviceName) - // Resolve the dashboard link off the same base URL the auth/gateway flow uses, - // so non-prod environments (staging/dev) point at the matching dashboard. const resolvedDashboardBase = zooCodeBaseUrl?.replace(/\/$/, "") || "https://www.zoocode.dev" + const zooModels = useMemo(() => routerModels?.["zoo-gateway"] ?? {}, [routerModels]) + const modelIds = useMemo(() => Object.keys(zooModels), [zooModels]) + const resolvedDefaultModelId = useMemo(() => pickZooGatewayDefaultModelId(modelIds), [modelIds]) + + useEffect(() => { + if (modelIds.length === 0) { + return + } + + const current = apiConfiguration.zooGatewayModelId + if (!current || !modelIds.includes(current)) { + setApiConfigurationField("zooGatewayModelId", resolvedDefaultModelId) + } + }, [apiConfiguration.zooGatewayModelId, modelIds, resolvedDefaultModelId, setApiConfigurationField]) + return ( <> - {/* Zoo Code Authentication Section */}
@@ -72,8 +111,8 @@ export const ZooGateway = ({ Date: Thu, 28 May 2026 14:32:14 -0600 Subject: [PATCH 13/20] test(zoo-gateway): cover dynamic default model picker for codecov patch Exports pickZooGatewayDefaultModelId so the helper is unit-testable and adds component tests for the auto-default useEffect (no-op while catalog loads, auto-pick on empty profile, repair stale id, no-op when valid). Co-authored-by: Cursor --- .../settings/providers/ZooGateway.tsx | 3 +- .../providers/__tests__/ZooGateway.spec.tsx | 168 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index e6a24e45f3..004aed92a0 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -25,7 +25,8 @@ function isSonnet45ModelId(id: string) { return /sonnet-4[.-]5|sonnet-4\.5/i.test(id) } -function pickZooGatewayDefaultModelId(modelIds: string[]) { +// Exported for unit tests. +export function pickZooGatewayDefaultModelId(modelIds: string[]) { if (modelIds.length === 0) { return zooGatewayDefaultModelId } diff --git a/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx new file mode 100644 index 0000000000..8be3b24e7d --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx @@ -0,0 +1,168 @@ +import React from "react" +import { render, waitFor } from "@/utils/test-utils" +import type { ModelInfo, ProviderSettings, RouterModels } from "@roo-code/types" + +import { ZooGateway, pickZooGatewayDefaultModelId } from "../ZooGateway" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + zooCodeIsAuthenticated: true, + zooCodeUserEmail: "user@example.com", + zooCodeUserName: "User", + zooCodeBaseUrl: "https://www.zoocode.dev", + uriScheme: "vscode", + deviceName: "Test Device", + }), +})) + +vi.mock("@src/oauth/urls", () => ({ + getZooCodeAuthUrl: () => "https://www.zoocode.dev/dashboard/connect", +})) + +vi.mock("../../ModelPicker", () => ({ + ModelPicker: ({ defaultModelId }: { defaultModelId: string }) => ( +
+ ), +})) + +const baseInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1, + outputPrice: 2, +} + +function buildRouterModels(modelIds: string[]): RouterModels { + const models = Object.fromEntries(modelIds.map((id) => [id, baseInfo])) + return { "zoo-gateway": models } as unknown as RouterModels +} + +describe("pickZooGatewayDefaultModelId", () => { + it("falls back to the static default when the catalog is empty", () => { + expect(pickZooGatewayDefaultModelId([])).toBe("anthropic/claude-sonnet-4") + }) + + it("prefers an exact anthropic/claude-sonnet-4.5 match", () => { + const result = pickZooGatewayDefaultModelId([ + "anthropic/claude-sonnet-4", + "anthropic/claude-sonnet-4.5", + "openai/gpt-4o", + ]) + expect(result).toBe("anthropic/claude-sonnet-4.5") + }) + + it("matches a Bedrock-style claude-sonnet-4-5 id", () => { + const result = pickZooGatewayDefaultModelId([ + "anthropic.claude-sonnet-4-20250514-v1:0", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + ]) + expect(result).toBe("anthropic.claude-sonnet-4-5-20250929-v1:0") + }) + + it("falls back to claude sonnet 4 when 4.5 is not in the catalog", () => { + const result = pickZooGatewayDefaultModelId(["openai/gpt-4o", "anthropic/claude-sonnet-4"]) + expect(result).toBe("anthropic/claude-sonnet-4") + }) + + it("falls back to the first available id when no claude sonnet is present", () => { + const result = pickZooGatewayDefaultModelId(["openai/gpt-4o", "google/gemini-2.5-pro"]) + expect(result).toBe("openai/gpt-4o") + }) +}) + +describe("ZooGateway component", () => { + const baseProps = { + organizationAllowList: { allowAll: true, providers: {} } as ProviderSettings extends never ? never : any, + setApiConfigurationField: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("auto-selects the resolved default model when the profile has no model id", async () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + await waitFor(() => { + expect(setApiConfigurationField).toHaveBeenCalledWith("zooGatewayModelId", "anthropic/claude-sonnet-4.5") + }) + }) + + it("reassigns a stale model id that is not in the catalog", async () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + await waitFor(() => { + expect(setApiConfigurationField).toHaveBeenCalledWith( + "zooGatewayModelId", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + ) + }) + }) + + it("does not overwrite a model id that is already valid for the catalog", async () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + await waitFor(() => { + expect(setApiConfigurationField).not.toHaveBeenCalled() + }) + }) + + it("does nothing while the catalog is still empty (router models loading)", () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + expect(setApiConfigurationField).not.toHaveBeenCalled() + }) +}) From 8522a759c6c2cd04f66f52f735be389a35052705 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 07:33:40 -0600 Subject: [PATCH 14/20] feat(zoo-gateway): auth callback, profile token sync, and sign-out Co-authored-by: Cursor --- src/activate/__tests__/handleUri.spec.ts | 93 +++++++++--- src/activate/handleUri.ts | 17 ++- src/core/webview/ClineProvider.ts | 136 +++++++++++++++++- .../webview/__tests__/ClineProvider.spec.ts | 13 +- .../__tests__/webviewMessageHandler.spec.ts | 66 ++++++++- src/core/webview/webviewMessageHandler.ts | 71 +++++++++ 6 files changed, 362 insertions(+), 34 deletions(-) diff --git a/src/activate/__tests__/handleUri.spec.ts b/src/activate/__tests__/handleUri.spec.ts index 187d9eeeba..559093095d 100644 --- a/src/activate/__tests__/handleUri.spec.ts +++ b/src/activate/__tests__/handleUri.spec.ts @@ -6,25 +6,32 @@ vi.mock("vscode", () => ({ import * as vscode from "vscode" -const { mockGetVisibleInstance, mockHandleZooCodeAuthCallback, mockSetZooCodeUserInfo, mockVisibleProvider } = - vi.hoisted(() => { - const mockVisibleProvider = { - handleOpenRouterCallback: vi.fn(), - handleRequestyCallback: vi.fn(), - handleZooCodeCallback: vi.fn(), - } as any - - return { - mockGetVisibleInstance: vi.fn(() => mockVisibleProvider), - mockHandleZooCodeAuthCallback: vi.fn(), - mockSetZooCodeUserInfo: vi.fn(), - mockVisibleProvider, - } - }) +const { + mockGetVisibleInstance, + mockGetAllInstances, + mockHandleZooCodeAuthCallback, + mockSetZooCodeUserInfo, + mockVisibleProvider, +} = vi.hoisted(() => { + const mockVisibleProvider = { + handleOpenRouterCallback: vi.fn(), + handleRequestyCallback: vi.fn(), + handleZooCodeCallback: vi.fn(), + } as any + + return { + mockGetVisibleInstance: vi.fn(() => mockVisibleProvider), + mockGetAllInstances: vi.fn(() => [mockVisibleProvider]), + mockHandleZooCodeAuthCallback: vi.fn(), + mockSetZooCodeUserInfo: vi.fn(), + mockVisibleProvider, + } +}) vi.mock("../../core/webview/ClineProvider", () => ({ ClineProvider: { getVisibleInstance: mockGetVisibleInstance, + getAllInstances: mockGetAllInstances, }, })) @@ -39,6 +46,7 @@ describe("handleUri", () => { beforeEach(() => { vi.clearAllMocks() mockGetVisibleInstance.mockReturnValue(mockVisibleProvider) + mockGetAllInstances.mockReturnValue([mockVisibleProvider]) }) it("ignores legacy cloud auth callback", async () => { @@ -54,8 +62,9 @@ describe("handleUri", () => { ) }) - it("stores callback user info even when no webview is visible", async () => { + it("stores callback user info even when no provider instances exist", async () => { mockGetVisibleInstance.mockReturnValue(null) + mockGetAllInstances.mockReturnValue([]) mockHandleZooCodeAuthCallback.mockResolvedValue(true) await handleUri({ @@ -69,6 +78,7 @@ describe("handleUri", () => { email: "jane@example.com", image: "https://example.com/avatar.png", }) + // No provider instances exist, so handleZooCodeCallback should not be called expect(mockVisibleProvider.handleZooCodeCallback).not.toHaveBeenCalled() }) @@ -116,4 +126,55 @@ describe("handleUri", () => { expect(mockSetZooCodeUserInfo).not.toHaveBeenCalled() expect(mockVisibleProvider.handleZooCodeCallback).not.toHaveBeenCalled() }) + + it("propagates the callback token to every ClineProvider instance, not just the visible one", async () => { + // Regression: prior to multi-instance fan-out, hidden providers (sidebar collapsed, + // secondary panels) never received the zooSessionToken, so their profile settings + // stayed unauthenticated until reload. + mockHandleZooCodeAuthCallback.mockResolvedValue(true) + + const hiddenProvider = { handleZooCodeCallback: vi.fn() } as any + const secondHidden = { handleZooCodeCallback: vi.fn() } as any + mockGetAllInstances.mockReturnValue([mockVisibleProvider, hiddenProvider, secondHidden]) + + await handleUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token", + } as any) + + expect(mockHandleZooCodeAuthCallback).toHaveBeenCalledWith("zoo_ext_test_token") + expect(mockSetZooCodeUserInfo).toHaveBeenCalled() + expect(mockVisibleProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token") + expect(hiddenProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token") + expect(secondHidden.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token") + }) + + it("serializes callbacks across instances to avoid concurrent profile-store writes", async () => { + // Regression: a previous implementation used Promise.all which fanned out concurrent + // read-modify-write operations on the same provider settings store. Verify the + // callbacks are invoked sequentially. + mockHandleZooCodeAuthCallback.mockResolvedValue(true) + + const order: string[] = [] + const makeProvider = (name: string) => + ({ + handleZooCodeCallback: vi.fn(async () => { + order.push(`${name}:start`) + // Yield to the event loop so a concurrent call would interleave. + await new Promise((resolve) => setTimeout(resolve, 0)) + order.push(`${name}:end`) + }), + }) as any + + const a = makeProvider("a") + const b = makeProvider("b") + mockGetAllInstances.mockReturnValue([a, b]) + + await handleUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token", + } as any) + + expect(order).toEqual(["a:start", "a:end", "b:start", "b:end"]) + }) }) diff --git a/src/activate/handleUri.ts b/src/activate/handleUri.ts index 523d254bc3..f1568a42cd 100644 --- a/src/activate/handleUri.ts +++ b/src/activate/handleUri.ts @@ -50,9 +50,20 @@ export const handleUri = async (uri: vscode.Uri) => { email, image, }) - // Refresh webview state if a panel is currently open - if (visibleProvider) { - await visibleProvider.handleZooCodeCallback(token) + // Write the token to all active provider instances regardless of visibility. + // The profile settings write (handleZooCodeCallback) must run on any active + // instance — not just the visible one — so the zoo-gateway zooSessionToken + // is persisted even when the sidebar/panel is hidden at callback time. + // + // Run sequentially (NOT Promise.all): each ClineProvider's + // handleZooCodeCallback does a read-modify-write on the same backing + // provider settings store (listConfig → getProfile → saveConfig / + // upsertProviderProfile). Fanning out concurrently across N instances + // can interleave reads/writes and clobber updates. Serialization here + // is cheap (at most a handful of instances) and avoids the race. + const allInstances = ClineProvider.getAllInstances() + for (const instance of allInstances) { + await instance.handleZooCodeCallback(token) } } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3f5af94cae..68df37f899 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -620,6 +620,10 @@ export class ClineProvider return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } + public static getAllInstances(): ClineProvider[] { + return Array.from(this.activeInstances) + } + public static async getInstance(): Promise { let visibleProvider = ClineProvider.getVisibleInstance() @@ -851,6 +855,64 @@ export class ClineProvider if (!currentTask || currentTask.abandoned || currentTask.abort) { await this.removeClineFromStack() } + + // Ensure zoo-gateway profile is seeded for users who signed in before this feature existed. + // Without this, users with a valid cached token but no zoo-gateway profile would need to + // re-authenticate to use Zoo Gateway. Fire-and-forget to avoid blocking webview init. + void this.ensureZooGatewayProfileSeeded().catch((err) => { + this.log(`[ensureZooGatewayProfileSeeded] Error: ${err instanceof Error ? err.message : String(err)}`) + }) + } + + /** + * Seeds the zoo-gateway provider profile for users who have a cached auth token + * but no profile (e.g., users who signed in before Zoo Gateway was added), or + * who have an empty/imported profile without a token. + * Called once per webview init; handleZooCodeCallback is idempotent so repeated calls are safe. + */ + private async ensureZooGatewayProfileSeeded(): Promise { + const { getCachedZooCodeToken } = await import("../../services/zoo-code-auth") + const token = getCachedZooCodeToken() + if (!token) return + + // Check ALL zoo-gateway profiles — only skip seeding if every profile has the current token. + // Using .find() would miss stale tokens in duplicate/renamed profiles since handleZooCodeCallback + // uses .filter() and updates all of them — the early-return guard must match. + const allProfiles = await this.providerSettingsManager.listConfig() + const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + + if (zooGatewayProfiles.length === 0) { + this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") + } else { + let allUpToDate = true + + for (const entry of zooGatewayProfiles) { + try { + const fullProfile = await this.providerSettingsManager.getProfile({ name: entry.name }) + if (fullProfile.zooSessionToken !== token) { + allUpToDate = false + this.log( + fullProfile.zooSessionToken + ? "[ensureZooGatewayProfileSeeded] Token mismatch (stale session?), updating with current token" + : "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token", + ) + break + } + } catch { + allUpToDate = false + this.log("[ensureZooGatewayProfileSeeded] Failed to read existing profile, will re-seed") + break + } + } + + if (allUpToDate) { + // All profiles have the current token — nothing to do + return + } + } + + // User has token but either no profile, some profiles without token, or stale tokens — seed all + await this.handleZooCodeCallback(token) } public async createTaskWithHistoryItem( @@ -1641,12 +1703,80 @@ export class ClineProvider await this.upsertProviderProfile(currentApiConfigName, newConfiguration) } - // Zoo Code Auth (for observability telemetry) + // Zoo Code Auth - async handleZooCodeCallback(_token: string) { + async handleZooCodeCallback(token: string) { // Auth mutation (token storage, subscription check, success toast) was already // performed by handleAuthCallback() in handleUri.ts before this method was called. - // This method only needs to refresh the webview state to reflect the new auth status. + // Save the zoo-gateway provider profile with the session token so that + // ZooGatewayHandler can authenticate without any manual user input. + // + // activate: true ONLY if Zoo Gateway is already the active profile — this pushes + // the new token to the in-memory handler so the current task picks it up immediately. + // Otherwise activate: false — do NOT switch providers mid-conversation. The user + // must explicitly select Zoo Gateway in settings if they want to use it. + try { + const { apiConfiguration } = await this.getState() + const currentSettings = this.contextProxy.getProviderSettings() + const currentApiConfigName = this.contextProxy.getValues().currentApiConfigName + + // Derive the gateway base URL from ZOO_CODE_BASE_URL so that non-prod environments + // (staging, local dev) route completions to the correct backend instead of always + // hard-coding production. An already-set value in the profile is NOT preserved here — + // it must always align with the auth server the user just authenticated against. + const { getZooCodeBaseUrl } = await import("../../services/zoo-code-auth") + const derivedGatewayBaseUrl = `${getZooCodeBaseUrl()}/api/gateway/v1` + + // Check if Zoo Gateway is the currently active profile by apiProvider identity, + // not by profile name (profile names are user-renameable). + const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + + // Always scan ALL profiles and update every zoo-gateway profile with the new token. + // This ensures renamed profiles, duplicate profiles, and inactive profiles all stay + // in sync. The model lookup in requestRouterModels uses .find() which returns the + // first zoo-gateway profile it finds — if that profile has a stale token, requests fail. + const allProfiles = await this.providerSettingsManager.listConfig() + const zooProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + + if (zooProfiles.length === 0) { + // No existing zoo-gateway profile — create the canonical default. + const newConfiguration: ProviderSettings = { + apiProvider: "zoo-gateway", + zooSessionToken: token, + zooGatewayModelId: apiConfiguration.zooGatewayModelId, + zooGatewayBaseUrl: derivedGatewayBaseUrl, + } + // Activate only if zoo-gateway was the active provider (shouldn't happen if + // no profiles exist, but defensive). + await this.upsertProviderProfile("Zoo Gateway", newConfiguration, isZooGatewayActive) + } else { + // Update every existing zoo-gateway profile with the new token and the + // derived base URL so that environment-specific routing stays consistent. + for (const entry of zooProfiles) { + const isActiveProfile = isZooGatewayActive && entry.name === currentApiConfigName + const existing = await this.providerSettingsManager.getProfile({ name: entry.name }) + const updated: ProviderSettings = { + ...existing, + zooSessionToken: token, + zooGatewayBaseUrl: derivedGatewayBaseUrl, + } + if (isActiveProfile) { + // Use upsertProviderProfile with activate: true so the in-memory handler + // picks up the new token immediately for the current task. + await this.upsertProviderProfile(entry.name, updated, true) + } else { + // Non-active profiles just need the token saved to disk. + await this.providerSettingsManager.saveConfig(entry.name, updated) + } + } + } + } catch (error) { + this.log( + `[handleZooCodeCallback] Failed to save zoo-gateway profile: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + } await this.postStateToWebview() } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 3a4858edad..279ea2d855 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2473,11 +2473,11 @@ describe("ClineProvider - Router Models", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, litellm: mockModels, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, - "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -2509,6 +2509,7 @@ describe("ClineProvider - Router Models", () => { .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail .mockResolvedValueOnce(mockModels) // unbound success .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success + .mockResolvedValueOnce(mockModels) // zoo-gateway success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail await messageHandler({ type: "requestRouterModels" }) @@ -2520,11 +2521,11 @@ describe("ClineProvider - Router Models", () => { openrouter: mockModels, requesty: {}, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, ollama: {}, lmstudio: {}, litellm: {}, - "vercel-ai-gateway": mockModels, - "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -2616,11 +2617,11 @@ describe("ClineProvider - Router Models", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, litellm: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, - "zoo-gateway": {}, poe: {}, deepseek: {}, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 32e0f9b48c..47dfb99b3c 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -365,11 +365,11 @@ describe("webviewMessageHandler - requestRouterModels", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, litellm: mockModels, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, - "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -377,6 +377,58 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) }) + it("recovers zoo-gateway credentials from a separate profile when not the active provider", async () => { + // Regression: when zoo-gateway is NOT the active provider, the active apiConfiguration + // will not contain zooSessionToken / zooGatewayBaseUrl. The aggregate model fetcher must + // fall back to scanning providerSettingsManager profiles for a zoo-gateway entry so the + // model picker still populates. + mockClineProvider.getState = vi.fn().mockResolvedValue({ + apiConfiguration: { + // Active provider is anthropic (or anything other than zoo-gateway). + apiProvider: "anthropic", + openRouterApiKey: "openrouter-key", + requestyApiKey: "requesty-key", + // No zooSessionToken / zooGatewayBaseUrl on the active config. + }, + }) + ;(mockClineProvider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([ + { name: "Anthropic", apiProvider: "anthropic" }, + { name: "My Zoo Gateway Profile", apiProvider: "zoo-gateway" }, + ]), + getProfile: vi.fn().mockResolvedValue({ + apiProvider: "zoo-gateway", + zooSessionToken: "recovered-zoo-token", + zooGatewayBaseUrl: "https://zoo.example.com/api/gateway/v1", + }), + } + + const mockModels: ModelRecord = { + "model-1": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "Test model 1", + }, + } + mockGetModels.mockResolvedValue(mockModels) + + await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels" }) + + expect((mockClineProvider as any).providerSettingsManager.listConfig).toHaveBeenCalled() + expect((mockClineProvider as any).providerSettingsManager.getProfile).toHaveBeenCalledWith({ + name: "My Zoo Gateway Profile", + }) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "zoo-gateway", + apiKey: "recovered-zoo-token", + baseUrl: "https://zoo.example.com/api/gateway/v1", + }) + + // Reset to avoid bleeding into other tests + delete (mockClineProvider as any).providerSettingsManager + }) + it("handles LiteLLM models with values from message when config is missing", async () => { mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { @@ -452,11 +504,11 @@ describe("webviewMessageHandler - requestRouterModels", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, litellm: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, - "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -480,6 +532,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("Requesty API error")) // requesty .mockResolvedValueOnce(mockModels) // unbound .mockResolvedValueOnce(mockModels) // vercel-ai-gateway + .mockResolvedValueOnce(mockModels) // zoo-gateway .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm await webviewMessageHandler(mockClineProvider, { @@ -508,11 +561,11 @@ describe("webviewMessageHandler - requestRouterModels", () => { openrouter: mockModels, requesty: {}, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, litellm: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, - "zoo-gateway": {}, poe: {}, deepseek: {}, }, @@ -527,6 +580,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("Requesty API error")) // requesty .mockRejectedValueOnce(new Error("Unbound error")) // unbound .mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway + .mockRejectedValueOnce(new Error("Zoo Gateway error")) // zoo-gateway .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm await webviewMessageHandler(mockClineProvider, { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 10141c1c87..a59ea3da88 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -945,6 +945,28 @@ export const webviewMessageHandler = async ( } } + // For zoo-gateway, the token may be stored in a separate zoo-gateway profile + // (not the currently active profile). Look it up so the model list populates + // even when zoo-gateway isn't the active provider. + let zooGatewayToken = apiConfiguration.zooSessionToken + let zooGatewayBaseUrl = apiConfiguration.zooGatewayBaseUrl + + if (!zooGatewayToken) { + try { + const allProfiles = await provider.providerSettingsManager.listConfig() + const zooGatewayProfile = allProfiles.find((p) => p.apiProvider === "zoo-gateway") + if (zooGatewayProfile) { + const fullProfile = await provider.providerSettingsManager.getProfile({ + name: zooGatewayProfile.name, + }) + zooGatewayToken = fullProfile.zooSessionToken + zooGatewayBaseUrl = fullProfile.zooGatewayBaseUrl ?? zooGatewayBaseUrl + } + } catch (error) { + console.debug("Failed to look up zoo-gateway profile for model fetch:", error) + } + } + // Base candidates (only those handled by this aggregate fetcher) const candidates: { key: RouterName; options: GetModelsOptions }[] = [ { key: "openrouter", options: { provider: "openrouter" } }, @@ -964,6 +986,14 @@ export const webviewMessageHandler = async ( }, }, { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, + { + key: "zoo-gateway", + options: { + provider: "zoo-gateway", + apiKey: zooGatewayToken, + baseUrl: zooGatewayBaseUrl, + }, + }, ] // LiteLLM is conditional on baseUrl+apiKey @@ -2432,6 +2462,47 @@ export const webviewMessageHandler = async ( try { const { disconnectZooCode } = await import("../../services/zoo-code-auth") await disconnectZooCode() + + // Clear zooSessionToken from ALL provider profiles with apiProvider === "zoo-gateway". + // Profiles are user-renameable, so we cannot rely on a hardcoded name like "Zoo Gateway". + // We must scan all profiles and clear tokens from any that use the zoo-gateway provider. + try { + const allProfiles = await provider.providerSettingsManager.listConfig() + // Check if Zoo Gateway is the currently active profile by apiProvider identity + const currentSettings = provider.contextProxy.getProviderSettings() + const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + const currentApiConfigName = provider.contextProxy.getValues().currentApiConfigName + + for (const entry of allProfiles) { + if (entry.apiProvider === "zoo-gateway") { + const profile = await provider.providerSettingsManager.getProfile({ name: entry.name }) + const { zooSessionToken: _removed, ...cleanedProfile } = profile + + // If this is the currently active profile, ALWAYS push to the in-memory + // handler — even when the persisted profile has already been cleared — + // because currentSettings (and therefore the live API handler) may still + // carry a stale token from before sign-out. Persisted-only profiles get + // rewritten only when they previously had a token to avoid no-op disk writes. + const isThisProfileActive = isZooGatewayActive && currentApiConfigName === entry.name + + if (isThisProfileActive) { + await provider.upsertProviderProfile(entry.name, cleanedProfile, true) + provider.log( + `[zooCodeSignOut] Cleared zooSessionToken from "${entry.name}" profile and updated in-memory handler`, + ) + } else if (profile.zooSessionToken) { + await provider.providerSettingsManager.saveConfig(entry.name, cleanedProfile) + provider.log(`[zooCodeSignOut] Cleared zooSessionToken from "${entry.name}" profile`) + } + } + } + } catch (profileError) { + // Log but don't fail the sign-out if profile cleanup fails + provider.log( + `[zooCodeSignOut] Failed to clear profile token: ${profileError instanceof Error ? profileError.message : String(profileError)}`, + ) + } + await provider.postStateToWebview() } catch (error) { provider.log( From 2319209995c3cd0fafcbc92673bc2bf4e3dd9bc1 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 09:53:14 -0600 Subject: [PATCH 15/20] test(zoo-gateway): cover auth callback profile sync and sign-out Add ClineProvider tests for handleZooCodeCallback, ensureZooGatewayProfileSeeded, and webviewMessageHandler zooCodeSignOut to satisfy codecov patch on PR #347. Co-authored-by: Cursor --- .../webview/__tests__/ClineProvider.spec.ts | 155 ++++++++++++++++++ .../__tests__/webviewMessageHandler.spec.ts | 81 +++++++++ 2 files changed, 236 insertions(+) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 279ea2d855..e6485d70d5 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -230,6 +230,14 @@ vi.mock("../../../api/providers/fetchers/modelCache", () => ({ getModelsFromCache: vi.fn().mockReturnValue(undefined), })) +vi.mock("../../../services/zoo-code-auth", () => ({ + getZooCodeBaseUrl: vi.fn(() => "https://www.zoocode.dev"), + getCachedZooCodeToken: vi.fn(), + handleAuthCallback: vi.fn(), + setZooCodeUserInfo: vi.fn(), + disconnectZooCode: vi.fn(), +})) + vi.mock("../../../shared/modes", () => ({ modes: [ { @@ -3661,4 +3669,151 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { vi.mocked(fsUtils.fileExistsAtPath).mockRestore() }) }) + + describe("Zoo Code auth profile sync", () => { + beforeEach(async () => { + const { getCachedZooCodeToken } = await import("../../../services/zoo-code-auth") + vi.mocked(getCachedZooCodeToken).mockReturnValue("") + }) + + describe("handleZooCodeCallback", () => { + it("creates a Zoo Gateway profile when none exists", async () => { + vi.spyOn(provider, "getState").mockResolvedValue({ + apiConfiguration: { zooGatewayModelId: "anthropic/claude-sonnet-4" }, + } as any) + vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({ + apiProvider: "anthropic", + } as any) + vi.spyOn(provider.contextProxy, "getValues").mockReturnValue({ + currentApiConfigName: "Anthropic", + } as any) + const upsertSpy = vi.spyOn(provider, "upsertProviderProfile").mockResolvedValue("profile-id") + vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined) + ;(provider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([]), + } + + await provider.handleZooCodeCallback("zoo_ext_token") + + expect(upsertSpy).toHaveBeenCalledWith( + "Zoo Gateway", + expect.objectContaining({ + apiProvider: "zoo-gateway", + zooSessionToken: "zoo_ext_token", + zooGatewayBaseUrl: "https://www.zoocode.dev/api/gateway/v1", + }), + false, + ) + }) + + it("updates every zoo-gateway profile and activates only the active one", async () => { + vi.spyOn(provider, "getState").mockResolvedValue({ + apiConfiguration: { zooGatewayModelId: "anthropic/claude-sonnet-4" }, + } as any) + vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({ + apiProvider: "zoo-gateway", + } as any) + vi.spyOn(provider.contextProxy, "getValues").mockReturnValue({ + currentApiConfigName: "Zoo Gateway", + } as any) + const upsertSpy = vi.spyOn(provider, "upsertProviderProfile").mockResolvedValue("profile-id") + const saveConfig = vi.fn().mockResolvedValue(undefined) + vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined) + ;(provider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([ + { name: "Zoo Gateway", apiProvider: "zoo-gateway" }, + { name: "Backup Zoo", apiProvider: "zoo-gateway" }, + ]), + getProfile: vi + .fn() + .mockResolvedValueOnce({ + apiProvider: "zoo-gateway", + zooSessionToken: "old-token", + zooGatewayBaseUrl: "https://old.example/api/gateway/v1", + }) + .mockResolvedValueOnce({ + apiProvider: "zoo-gateway", + zooSessionToken: "old-token", + }), + saveConfig, + } + + await provider.handleZooCodeCallback("new-token") + + expect(upsertSpy).toHaveBeenCalledWith( + "Zoo Gateway", + expect.objectContaining({ + zooSessionToken: "new-token", + zooGatewayBaseUrl: "https://www.zoocode.dev/api/gateway/v1", + }), + true, + ) + expect(saveConfig).toHaveBeenCalledWith( + "Backup Zoo", + expect.objectContaining({ + zooSessionToken: "new-token", + zooGatewayBaseUrl: "https://www.zoocode.dev/api/gateway/v1", + }), + ) + }) + + it("logs and posts state when profile persistence fails", async () => { + vi.spyOn(provider, "getState").mockRejectedValue(new Error("state unavailable")) + vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined) + ;(provider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([]), + } + + await provider.handleZooCodeCallback("zoo_ext_token") + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("[handleZooCodeCallback] Failed to save zoo-gateway profile"), + ) + }) + }) + + describe("ensureZooGatewayProfileSeeded", () => { + it("does nothing when no cached auth token exists", async () => { + const handleSpy = vi.spyOn(provider, "handleZooCodeCallback").mockResolvedValue(undefined) + + ;(provider as any).providerSettingsManager = { + listConfig: vi.fn(), + } + + await (provider as any).ensureZooGatewayProfileSeeded() + + expect(handleSpy).not.toHaveBeenCalled() + }) + + it("skips seeding when every zoo-gateway profile already has the current token", async () => { + const { getCachedZooCodeToken } = await import("../../../services/zoo-code-auth") + vi.mocked(getCachedZooCodeToken).mockReturnValue("current-token") + const handleSpy = vi.spyOn(provider, "handleZooCodeCallback").mockResolvedValue(undefined) + + ;(provider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: "zoo-gateway" }]), + getProfile: vi.fn().mockResolvedValue({ zooSessionToken: "current-token" }), + } + + await (provider as any).ensureZooGatewayProfileSeeded() + + expect(handleSpy).not.toHaveBeenCalled() + }) + + it("re-seeds when any zoo-gateway profile has a stale or missing token", async () => { + const { getCachedZooCodeToken } = await import("../../../services/zoo-code-auth") + vi.mocked(getCachedZooCodeToken).mockReturnValue("fresh-token") + const handleSpy = vi.spyOn(provider, "handleZooCodeCallback").mockResolvedValue(undefined) + + ;(provider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: "zoo-gateway" }]), + getProfile: vi.fn().mockResolvedValue({ zooSessionToken: "stale-token" }), + } + + await (provider as any).ensureZooGatewayProfileSeeded() + + expect(handleSpy).toHaveBeenCalledWith("fresh-token") + }) + }) + }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 47dfb99b3c..4695a93c78 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -4,6 +4,9 @@ import type { Mock } from "vitest" // Mock dependencies - must come before imports vi.mock("../../../api/providers/fetchers/modelCache") +vi.mock("../../../services/zoo-code-auth", () => ({ + disconnectZooCode: vi.fn().mockResolvedValue(undefined), +})) vi.mock("../../../api/providers/fetchers/lmstudio", () => ({ getLMStudioModels: vi.fn(), })) @@ -1146,3 +1149,81 @@ describe("webviewMessageHandler - downloadErrorDiagnostics", () => { expect(generateErrorDiagnostics).not.toHaveBeenCalled() }) }) + +describe("zooCodeSignOut", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("disconnects Zoo Code and clears tokens from all zoo-gateway profiles", async () => { + const { disconnectZooCode } = await import("../../../services/zoo-code-auth") + const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) + const saveConfig = vi.fn().mockResolvedValue(undefined) + + ;(mockClineProvider as any).contextProxy = { + ...mockClineProvider.contextProxy, + getProviderSettings: vi.fn().mockReturnValue({ apiProvider: "zoo-gateway" }), + getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), + } + ;(mockClineProvider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([ + { name: "Zoo Gateway", apiProvider: "zoo-gateway" }, + { name: "Backup Zoo", apiProvider: "zoo-gateway" }, + ]), + getProfile: vi + .fn() + .mockResolvedValueOnce({ + apiProvider: "zoo-gateway", + zooSessionToken: "token-active", + zooGatewayModelId: "anthropic/claude-sonnet-4", + }) + .mockResolvedValueOnce({ + apiProvider: "zoo-gateway", + zooSessionToken: "token-backup", + }), + saveConfig, + } + ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile + + await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) + + expect(disconnectZooCode).toHaveBeenCalled() + expect(upsertProviderProfile).toHaveBeenCalledWith( + "Zoo Gateway", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + true, + ) + expect(saveConfig).toHaveBeenCalledWith( + "Backup Zoo", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + ) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("still clears the in-memory handler when the active profile token is already empty on disk", async () => { + const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) + + ;(mockClineProvider as any).contextProxy = { + ...mockClineProvider.contextProxy, + getProviderSettings: vi.fn().mockReturnValue({ apiProvider: "zoo-gateway" }), + getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), + } + ;(mockClineProvider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: "zoo-gateway" }]), + getProfile: vi.fn().mockResolvedValue({ + apiProvider: "zoo-gateway", + zooGatewayModelId: "anthropic/claude-sonnet-4", + }), + saveConfig: vi.fn(), + } + ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile + + await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) + + expect(upsertProviderProfile).toHaveBeenCalledWith( + "Zoo Gateway", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + true, + ) + }) +}) From 7e35b1b39fa5bd7d8249f1145c427e133e119cfd Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 28 May 2026 13:58:46 -0600 Subject: [PATCH 16/20] 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 --- src/api/providers/zoo-gateway.ts | 136 ++++++++++++++++++++++++------- src/i18n/locales/en/common.json | 11 ++- 2 files changed, 116 insertions(+), 31 deletions(-) diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index 28c1940033..a3aaa1648d 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -1,3 +1,4 @@ +import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" @@ -9,8 +10,9 @@ import { } from "@roo-code/types" import { ApiHandlerOptions } from "../../shared/api" -import { getCachedZooCodeToken, getZooCodeBaseUrl } from "../../services/zoo-code-auth" +import { clearZooCodeToken, getCachedZooCodeToken, getZooCodeBaseUrl } from "../../services/zoo-code-auth" import { Package } from "../../shared/package" +import { t } from "../../i18n" import { ApiStream } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" @@ -19,6 +21,74 @@ import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { RouterProvider } from "./router-provider" +function getApiErrorStatus(error: unknown): number | undefined { + if (typeof error === "object" && error !== null && "status" in error) { + const status = (error as { status: unknown }).status + if (typeof status === "number") return status + } + return undefined +} + +function getApiErrorCode(error: unknown): string | undefined { + const err = error as { code?: unknown; error?: { code?: unknown } } | null + if (!err) return undefined + if (typeof err.code === "string") return err.code + if (typeof err.error?.code === "string") return err.error.code + return undefined +} + +function buildZooCodeSignInUrl(): string { + const callbackUri = encodeURIComponent( + `${vscode.env.uriScheme}://${Package.publisher}.${Package.name}/auth-callback`, + ) + const device = encodeURIComponent(vscode.env.appName || "VS Code") + const editor = encodeURIComponent("VS Code") + return `${getZooCodeBaseUrl()}/dashboard/connect?device=${device}&editor=${editor}&version=${Package.version}&callback_uri=${callbackUri}` +} + +// Caller must always rethrow — this only surfaces UX, never swallows. +async function surfaceGatewayApiError(error: unknown): Promise { + const status = getApiErrorStatus(error) + if (status === undefined) return + const code = getApiErrorCode(error) + + if (status === 401) { + // Wipe before sign-in so the callback rebinds against an empty slot. + await clearZooCodeToken() + const action = await vscode.window.showErrorMessage( + t("common:zooAuth.errors.session_expired"), + t("common:zooAuth.buttons.sign_in"), + ) + if (action) { + void vscode.env.openExternal(vscode.Uri.parse(buildZooCodeSignInUrl())) + } + return + } + + const isBudgetExceeded = status === 429 && (code === "monthly_budget_exceeded" || code === "daily_budget_exceeded") + if (status === 402 || isBudgetExceeded) { + const message = isBudgetExceeded + ? t("common:zooAuth.errors.budget_exceeded") + : t("common:zooAuth.errors.out_of_credits") + const action = await vscode.window.showErrorMessage(message, t("common:zooAuth.buttons.add_credits")) + if (action) { + void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/dashboard/credits`)) + } + return + } + + if (status === 403) { + const action = await vscode.window.showErrorMessage( + t("common:zooAuth.errors.account_unavailable"), + t("common:zooAuth.buttons.contact_support"), + ) + if (action) { + void vscode.env.openExternal(vscode.Uri.parse(`${getZooCodeBaseUrl()}/support`)) + } + return + } +} + // Extend OpenAI's CompletionUsage to include Zoo Gateway specific fields (same as Vercel AI Gateway) interface ZooGatewayUsage extends OpenAI.CompletionUsage { cache_creation_input_tokens?: number @@ -100,43 +170,48 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio parallel_tool_calls: metadata?.parallelToolCalls ?? true, } - const completion = await this.client.chat.completions.create(body, { - headers: requestHeaders, - }) + try { + const completion = await this.client.chat.completions.create(body, { + headers: requestHeaders, + }) - for await (const chunk of completion) { - const delta = chunk.choices[0]?.delta - if (delta?.content) { - yield { - type: "text", - text: delta.content, + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } } - } - // Emit raw tool call chunks - NativeToolCallParser handles state management - if (delta?.tool_calls) { - for (const toolCall of delta.tool_calls) { - yield { - type: "tool_call_partial", - index: toolCall.index, - id: toolCall.id, - name: toolCall.function?.name, - arguments: toolCall.function?.arguments, + // Emit raw tool call chunks - NativeToolCallParser handles state management + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } } } - } - if (chunk.usage) { - const usage = chunk.usage as ZooGatewayUsage - yield { - type: "usage", - inputTokens: usage.prompt_tokens || 0, - outputTokens: usage.completion_tokens || 0, - cacheWriteTokens: usage.cache_creation_input_tokens || undefined, - cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined, - totalCost: usage.cost ?? 0, + if (chunk.usage) { + const usage = chunk.usage as ZooGatewayUsage + yield { + type: "usage", + inputTokens: usage.prompt_tokens || 0, + outputTokens: usage.completion_tokens || 0, + cacheWriteTokens: usage.cache_creation_input_tokens || undefined, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined, + totalCost: usage.cost ?? 0, + } } } + } catch (error) { + void surfaceGatewayApiError(error) + throw error } } @@ -159,6 +234,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio const response = await this.client.chat.completions.create(requestOptions) return response.choices[0]?.message.content || "" } catch (error) { + void surfaceGatewayApiError(error) if (error instanceof Error) { throw new Error(`Zoo Gateway completion error: ${error.message}`) } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 785e08e83b..5abed998a5 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -253,11 +253,20 @@ "invalid_token_received": "Zoo Code: Invalid authentication token received.", "token_verification_failed": "Zoo Code: Token verification failed.", "invalid_token": "Zoo Code: Invalid token.", - "could_not_verify_token": "Zoo Code: Could not verify token." + "could_not_verify_token": "Zoo Code: Could not verify token.", + "session_expired": "Zoo Code session expired. Please sign in again to continue using Zoo Gateway.", + "out_of_credits": "Zoo Gateway: insufficient credits. Add credits to continue.", + "account_unavailable": "Zoo Gateway: your account is currently unavailable. Please contact support.", + "budget_exceeded": "Zoo Gateway: usage budget reached. Wait for the budget to reset or top up." }, "info": { "connected": "Zoo Code: Successfully connected! You can now use Zoo Code as your AI provider.", "disconnected": "Zoo Code: Disconnected successfully." + }, + "buttons": { + "sign_in": "Sign In", + "add_credits": "Add Credits", + "contact_support": "Contact Support" } }, "codeActions": { From c4bd80c0e5c307d9064045a6297f6303a71096e2 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 28 May 2026 14:43:10 -0600 Subject: [PATCH 17/20] i18n(zoo-gateway): backfill zooAuth translations for 17 non-English locales Adds session_expired, out_of_credits, account_unavailable, budget_exceeded under zooAuth.errors and a new zooAuth.buttons block (sign_in, add_credits, contact_support) introduced by the gateway 401/402/403 UX so check-translations passes. Co-authored-by: Cursor --- src/i18n/locales/ca/common.json | 11 ++++++++++- src/i18n/locales/de/common.json | 11 ++++++++++- src/i18n/locales/es/common.json | 11 ++++++++++- src/i18n/locales/fr/common.json | 11 ++++++++++- src/i18n/locales/hi/common.json | 11 ++++++++++- src/i18n/locales/id/common.json | 11 ++++++++++- src/i18n/locales/it/common.json | 11 ++++++++++- src/i18n/locales/ja/common.json | 11 ++++++++++- src/i18n/locales/ko/common.json | 11 ++++++++++- src/i18n/locales/nl/common.json | 11 ++++++++++- src/i18n/locales/pl/common.json | 11 ++++++++++- src/i18n/locales/pt-BR/common.json | 11 ++++++++++- src/i18n/locales/ru/common.json | 11 ++++++++++- src/i18n/locales/tr/common.json | 11 ++++++++++- src/i18n/locales/vi/common.json | 11 ++++++++++- src/i18n/locales/zh-CN/common.json | 11 ++++++++++- src/i18n/locales/zh-TW/common.json | 11 ++++++++++- 17 files changed, 170 insertions(+), 17 deletions(-) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index a8c0300337..0a1c99f1f8 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: S'ha rebut un token d'autenticació no vàlid.", "token_verification_failed": "Zoo Code: Ha fallat la verificació del token.", "invalid_token": "Zoo Code: Token no vàlid.", - "could_not_verify_token": "Zoo Code: No s'ha pogut verificar el token." + "could_not_verify_token": "Zoo Code: No s'ha pogut verificar el token.", + "session_expired": "Sessió de Zoo Code caducada. Torna a iniciar sessió per continuar utilitzant Zoo Gateway.", + "out_of_credits": "Zoo Gateway: crèdits insuficients. Afegeix crèdits per continuar.", + "account_unavailable": "Zoo Gateway: el teu compte no està disponible. Contacta amb el suport.", + "budget_exceeded": "Zoo Gateway: pressupost d'ús assolit. Espera que es restableixi o afegeix crèdits." }, "info": { "connected": "Zoo Code: Connectat correctament! Ara podeu utilitzar Zoo Code com a proveïdor d'IA.", "disconnected": "Zoo Code: Desconnectat correctament." + }, + "buttons": { + "sign_in": "Inicia sessió", + "add_credits": "Afegeix crèdits", + "contact_support": "Contacta amb el suport" } }, "codeActions": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 10961de9af..02f27925a5 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -256,11 +256,20 @@ "invalid_token_received": "Zoo Code: Ungültiger Authentifizierungstoken empfangen.", "token_verification_failed": "Zoo Code: Token-Überprüfung fehlgeschlagen.", "invalid_token": "Zoo Code: Ungültiger Token.", - "could_not_verify_token": "Zoo Code: Token konnte nicht verifiziert werden." + "could_not_verify_token": "Zoo Code: Token konnte nicht verifiziert werden.", + "session_expired": "Zoo Code-Sitzung abgelaufen. Bitte melde dich erneut an, um Zoo Gateway weiter zu nutzen.", + "out_of_credits": "Zoo Gateway: Unzureichendes Guthaben. Füge Guthaben hinzu, um fortzufahren.", + "account_unavailable": "Zoo Gateway: Dein Konto ist derzeit nicht verfügbar. Bitte kontaktiere den Support.", + "budget_exceeded": "Zoo Gateway: Nutzungsbudget erreicht. Warte auf das Zurücksetzen oder lade auf." }, "info": { "connected": "Zoo Code: Erfolgreich verbunden! Du kannst Zoo Code jetzt als KI-Anbieter verwenden.", "disconnected": "Zoo Code: Erfolgreich getrennt." + }, + "buttons": { + "sign_in": "Anmelden", + "add_credits": "Guthaben hinzufügen", + "contact_support": "Support kontaktieren" } }, "codeActions": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index afe7e29a9e..3c5d6ea754 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -256,11 +256,20 @@ "invalid_token_received": "Zoo Code: Se recibió un token de autenticación inválido.", "token_verification_failed": "Zoo Code: Error al verificar el token.", "invalid_token": "Zoo Code: Token inválido.", - "could_not_verify_token": "Zoo Code: No se pudo verificar el token." + "could_not_verify_token": "Zoo Code: No se pudo verificar el token.", + "session_expired": "Sesión de Zoo Code caducada. Inicia sesión de nuevo para seguir usando Zoo Gateway.", + "out_of_credits": "Zoo Gateway: créditos insuficientes. Añade créditos para continuar.", + "account_unavailable": "Zoo Gateway: tu cuenta no está disponible. Contacta con soporte.", + "budget_exceeded": "Zoo Gateway: presupuesto de uso alcanzado. Espera al restablecimiento o recarga." }, "info": { "connected": "Zoo Code: ¡Conectado correctamente! Ahora puedes usar Zoo Code como proveedor de IA.", "disconnected": "Zoo Code: Desconectado correctamente." + }, + "buttons": { + "sign_in": "Iniciar sesión", + "add_credits": "Añadir créditos", + "contact_support": "Contactar con soporte" } }, "codeActions": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 08c3a4ce82..7f9e466f04 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Jeton d'authentification invalide reçu.", "token_verification_failed": "Zoo Code: La vérification du jeton a échoué.", "invalid_token": "Zoo Code: Jeton invalide.", - "could_not_verify_token": "Zoo Code: Impossible de vérifier le jeton." + "could_not_verify_token": "Zoo Code: Impossible de vérifier le jeton.", + "session_expired": "Session Zoo Code expirée. Reconnectez-vous pour continuer à utiliser Zoo Gateway.", + "out_of_credits": "Zoo Gateway : crédits insuffisants. Ajoutez des crédits pour continuer.", + "account_unavailable": "Zoo Gateway : votre compte n'est pas disponible actuellement. Contactez le support.", + "budget_exceeded": "Zoo Gateway : budget d'utilisation atteint. Attendez la réinitialisation ou rechargez." }, "info": { "connected": "Zoo Code: Connecté avec succès ! Vous pouvez maintenant utiliser Zoo Code comme fournisseur d'IA.", "disconnected": "Zoo Code: Déconnecté avec succès." + }, + "buttons": { + "sign_in": "Se connecter", + "add_credits": "Ajouter des crédits", + "contact_support": "Contacter le support" } }, "codeActions": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 34a1114ef2..3c567dac08 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: अमान्य प्रमाणीकरण token प्राप्त हुआ।", "token_verification_failed": "Zoo Code: Token सत्यापन विफल रहा।", "invalid_token": "Zoo Code: अमान्य token।", - "could_not_verify_token": "Zoo Code: Token सत्यापित नहीं किया जा सका।" + "could_not_verify_token": "Zoo Code: Token सत्यापित नहीं किया जा सका।", + "session_expired": "Zoo Code सत्र समाप्त हो गया है। Zoo Gateway का उपयोग जारी रखने के लिए कृपया फिर से साइन इन करें।", + "out_of_credits": "Zoo Gateway: अपर्याप्त क्रेडिट। जारी रखने के लिए क्रेडिट जोड़ें।", + "account_unavailable": "Zoo Gateway: आपका खाता वर्तमान में उपलब्ध नहीं है। कृपया सहायता से संपर्क करें।", + "budget_exceeded": "Zoo Gateway: उपयोग बजट पूरा हो गया है। बजट रीसेट होने की प्रतीक्षा करें या रिचार्ज करें।" }, "info": { "connected": "Zoo Code: सफलतापूर्वक कनेक्ट हो गया! अब तुम Zoo Code को अपने AI प्रदाता के रूप में उपयोग कर सकते हो।", "disconnected": "Zoo Code: सफलतापूर्वक डिस्कनेक्ट हो गया।" + }, + "buttons": { + "sign_in": "साइन इन करें", + "add_credits": "क्रेडिट जोड़ें", + "contact_support": "सहायता से संपर्क करें" } }, "codeActions": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index faa610c89b..7208ea9507 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Token autentikasi yang diterima tidak valid.", "token_verification_failed": "Zoo Code: Verifikasi token gagal.", "invalid_token": "Zoo Code: Token tidak valid.", - "could_not_verify_token": "Zoo Code: Tidak dapat memverifikasi token." + "could_not_verify_token": "Zoo Code: Tidak dapat memverifikasi token.", + "session_expired": "Sesi Zoo Code kedaluwarsa. Silakan masuk lagi untuk melanjutkan menggunakan Zoo Gateway.", + "out_of_credits": "Zoo Gateway: kredit tidak cukup. Tambahkan kredit untuk melanjutkan.", + "account_unavailable": "Zoo Gateway: akun Anda sedang tidak tersedia. Silakan hubungi dukungan.", + "budget_exceeded": "Zoo Gateway: anggaran penggunaan tercapai. Tunggu reset anggaran atau isi ulang." }, "info": { "connected": "Zoo Code: Berhasil terhubung! Kamu sekarang bisa menggunakan Zoo Code sebagai penyedia AI.", "disconnected": "Zoo Code: Berhasil terputus." + }, + "buttons": { + "sign_in": "Masuk", + "add_credits": "Tambah Kredit", + "contact_support": "Hubungi Dukungan" } }, "codeActions": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index e3961e5901..a69a96092c 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Ricevuto un token di autenticazione non valido.", "token_verification_failed": "Zoo Code: Verifica del token non riuscita.", "invalid_token": "Zoo Code: Token non valido.", - "could_not_verify_token": "Zoo Code: Impossibile verificare il token." + "could_not_verify_token": "Zoo Code: Impossibile verificare il token.", + "session_expired": "Sessione Zoo Code scaduta. Accedi di nuovo per continuare a usare Zoo Gateway.", + "out_of_credits": "Zoo Gateway: crediti insufficienti. Aggiungi crediti per continuare.", + "account_unavailable": "Zoo Gateway: il tuo account non è attualmente disponibile. Contatta l'assistenza.", + "budget_exceeded": "Zoo Gateway: budget di utilizzo raggiunto. Attendi il reset o ricarica." }, "info": { "connected": "Zoo Code: Connesso con successo! Ora puoi usare Zoo Code come fornitore AI.", "disconnected": "Zoo Code: Disconnesso con successo." + }, + "buttons": { + "sign_in": "Accedi", + "add_credits": "Aggiungi crediti", + "contact_support": "Contatta l'assistenza" } }, "codeActions": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index a1804e4b8c..bea4db707c 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: 無効な認証 token を受信しました。", "token_verification_failed": "Zoo Code: Token の検証に失敗しました。", "invalid_token": "Zoo Code: 無効な token です。", - "could_not_verify_token": "Zoo Code: Token を検証できませんでした。" + "could_not_verify_token": "Zoo Code: Token を検証できませんでした。", + "session_expired": "Zoo Codeのセッションの有効期限が切れました。Zoo Gatewayを引き続き使用するには、再度サインインしてください。", + "out_of_credits": "Zoo Gateway: クレジットが不足しています。続行するにはクレジットを追加してください。", + "account_unavailable": "Zoo Gateway: 現在アカウントを利用できません。サポートまでお問い合わせください。", + "budget_exceeded": "Zoo Gateway: 利用予算に達しました。リセットを待つかチャージしてください。" }, "info": { "connected": "Zoo Code: 接続に成功しました!Zoo Code を AI プロバイダーとして使用できます。", "disconnected": "Zoo Code: 正常に切断されました。" + }, + "buttons": { + "sign_in": "サインイン", + "add_credits": "クレジットを追加", + "contact_support": "サポートに連絡" } }, "codeActions": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index edcc4ea312..daf9f69e4c 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: 잘못된 인증 token을 수신했습니다.", "token_verification_failed": "Zoo Code: Token 검증에 실패했습니다.", "invalid_token": "Zoo Code: 잘못된 token입니다.", - "could_not_verify_token": "Zoo Code: Token을 검증할 수 없습니다." + "could_not_verify_token": "Zoo Code: Token을 검증할 수 없습니다.", + "session_expired": "Zoo Code 세션이 만료되었습니다. Zoo Gateway를 계속 사용하려면 다시 로그인해 주세요.", + "out_of_credits": "Zoo Gateway: 크레딧이 부족합니다. 계속하려면 크레딧을 추가하세요.", + "account_unavailable": "Zoo Gateway: 현재 계정을 사용할 수 없습니다. 지원팀에 문의하세요.", + "budget_exceeded": "Zoo Gateway: 사용 한도에 도달했습니다. 한도가 재설정되기를 기다리거나 충전하세요." }, "info": { "connected": "Zoo Code: 연결에 성공했습니다! 이제 Zoo Code를 AI 제공자로 사용할 수 있습니다.", "disconnected": "Zoo Code: 연결이 해제되었습니다." + }, + "buttons": { + "sign_in": "로그인", + "add_credits": "크레딧 추가", + "contact_support": "지원팀 문의" } }, "codeActions": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 7dda89b7eb..3f4c1e984d 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Ongeldig authenticatietoken ontvangen.", "token_verification_failed": "Zoo Code: Tokenverificatie mislukt.", "invalid_token": "Zoo Code: Ongeldig token.", - "could_not_verify_token": "Zoo Code: Token kon niet worden geverifieerd." + "could_not_verify_token": "Zoo Code: Token kon niet worden geverifieerd.", + "session_expired": "Zoo Code-sessie verlopen. Meld je opnieuw aan om Zoo Gateway te blijven gebruiken.", + "out_of_credits": "Zoo Gateway: onvoldoende tegoed. Voeg tegoed toe om door te gaan.", + "account_unavailable": "Zoo Gateway: je account is momenteel niet beschikbaar. Neem contact op met support.", + "budget_exceeded": "Zoo Gateway: gebruiksbudget bereikt. Wacht op de reset of laad bij." }, "info": { "connected": "Zoo Code: Succesvol verbonden! Je kunt Zoo Code nu gebruiken als AI-provider.", "disconnected": "Zoo Code: Succesvol losgekoppeld." + }, + "buttons": { + "sign_in": "Aanmelden", + "add_credits": "Tegoed toevoegen", + "contact_support": "Contact opnemen" } }, "codeActions": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 152193755c..c88aabcaa5 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Otrzymano nieprawidłowy token uwierzytelniania.", "token_verification_failed": "Zoo Code: Weryfikacja tokenu nie powiodła się.", "invalid_token": "Zoo Code: Nieprawidłowy token.", - "could_not_verify_token": "Zoo Code: Nie udało się zweryfikować tokenu." + "could_not_verify_token": "Zoo Code: Nie udało się zweryfikować tokenu.", + "session_expired": "Sesja Zoo Code wygasła. Zaloguj się ponownie, aby kontynuować korzystanie z Zoo Gateway.", + "out_of_credits": "Zoo Gateway: niewystarczające środki. Doładuj, aby kontynuować.", + "account_unavailable": "Zoo Gateway: Twoje konto jest obecnie niedostępne. Skontaktuj się z pomocą techniczną.", + "budget_exceeded": "Zoo Gateway: osiągnięto budżet użycia. Poczekaj na reset lub doładuj." }, "info": { "connected": "Zoo Code: Połączono pomyślnie! Możesz teraz używać Zoo Code jako dostawcy AI.", "disconnected": "Zoo Code: Rozłączono pomyślnie." + }, + "buttons": { + "sign_in": "Zaloguj się", + "add_credits": "Doładuj", + "contact_support": "Kontakt z pomocą" } }, "codeActions": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index b72a4b75e9..ce4c51e08d 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Token de autenticação inválido recebido.", "token_verification_failed": "Zoo Code: Falha na verificação do token.", "invalid_token": "Zoo Code: Token inválido.", - "could_not_verify_token": "Zoo Code: Não foi possível verificar o token." + "could_not_verify_token": "Zoo Code: Não foi possível verificar o token.", + "session_expired": "Sessão do Zoo Code expirada. Entre novamente para continuar usando o Zoo Gateway.", + "out_of_credits": "Zoo Gateway: créditos insuficientes. Adicione créditos para continuar.", + "account_unavailable": "Zoo Gateway: sua conta está indisponível no momento. Entre em contato com o suporte.", + "budget_exceeded": "Zoo Gateway: orçamento de uso atingido. Aguarde o reset ou recarregue." }, "info": { "connected": "Zoo Code: Conectado com sucesso! Agora você pode usar o Zoo Code como provedor de IA.", "disconnected": "Zoo Code: Desconectado com sucesso." + }, + "buttons": { + "sign_in": "Entrar", + "add_credits": "Adicionar créditos", + "contact_support": "Contatar suporte" } }, "codeActions": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index f438184d3d..9d096fb346 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Получен недействительный token аутентификации.", "token_verification_failed": "Zoo Code: Не удалось проверить token.", "invalid_token": "Zoo Code: Недействительный token.", - "could_not_verify_token": "Zoo Code: Не удалось проверить token." + "could_not_verify_token": "Zoo Code: Не удалось проверить token.", + "session_expired": "Сессия Zoo Code истекла. Войдите снова, чтобы продолжить использовать Zoo Gateway.", + "out_of_credits": "Zoo Gateway: недостаточно кредитов. Добавьте кредиты, чтобы продолжить.", + "account_unavailable": "Zoo Gateway: ваша учётная запись сейчас недоступна. Обратитесь в поддержку.", + "budget_exceeded": "Zoo Gateway: лимит использования достигнут. Дождитесь сброса или пополните счёт." }, "info": { "connected": "Zoo Code: Успешно подключено! Теперь ты можешь использовать Zoo Code в качестве AI-провайдера.", "disconnected": "Zoo Code: Успешно отключено." + }, + "buttons": { + "sign_in": "Войти", + "add_credits": "Добавить кредиты", + "contact_support": "Связаться с поддержкой" } }, "codeActions": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index b02ce455c7..3d83d28c32 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: Geçersiz kimlik doğrulama token'ı alındı.", "token_verification_failed": "Zoo Code: Token doğrulaması başarısız oldu.", "invalid_token": "Zoo Code: Geçersiz token.", - "could_not_verify_token": "Zoo Code: Token doğrulanamadı." + "could_not_verify_token": "Zoo Code: Token doğrulanamadı.", + "session_expired": "Zoo Code oturumunun süresi doldu. Zoo Gateway'i kullanmaya devam etmek için lütfen tekrar giriş yapın.", + "out_of_credits": "Zoo Gateway: yetersiz kredi. Devam etmek için kredi ekleyin.", + "account_unavailable": "Zoo Gateway: hesabınız şu anda kullanılamıyor. Lütfen destek ile iletişime geçin.", + "budget_exceeded": "Zoo Gateway: kullanım bütçesine ulaşıldı. Sıfırlanmasını bekleyin veya kredi ekleyin." }, "info": { "connected": "Zoo Code: Başarıyla bağlandı! Artık Zoo Code'u AI sağlayıcısı olarak kullanabilirsin.", "disconnected": "Zoo Code: Başarıyla bağlantı kesildi." + }, + "buttons": { + "sign_in": "Giriş Yap", + "add_credits": "Kredi Ekle", + "contact_support": "Destek ile İletişim" } }, "codeActions": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index fbf9d64c14..d111ade94b 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -268,11 +268,20 @@ "invalid_token_received": "Zoo Code: Nhận được token xác thực không hợp lệ.", "token_verification_failed": "Zoo Code: Xác minh token thất bại.", "invalid_token": "Zoo Code: Token không hợp lệ.", - "could_not_verify_token": "Zoo Code: Không thể xác minh token." + "could_not_verify_token": "Zoo Code: Không thể xác minh token.", + "session_expired": "Phiên Zoo Code đã hết hạn. Vui lòng đăng nhập lại để tiếp tục sử dụng Zoo Gateway.", + "out_of_credits": "Zoo Gateway: không đủ tín dụng. Thêm tín dụng để tiếp tục.", + "account_unavailable": "Zoo Gateway: tài khoản của bạn hiện không khả dụng. Vui lòng liên hệ hỗ trợ.", + "budget_exceeded": "Zoo Gateway: đã đạt ngân sách sử dụng. Chờ đặt lại ngân sách hoặc nạp thêm." }, "info": { "connected": "Zoo Code: Kết nối thành công! Bạn có thể sử dụng Zoo Code làm nhà cung cấp AI.", "disconnected": "Zoo Code: Đã ngắt kết nối thành công." + }, + "buttons": { + "sign_in": "Đăng nhập", + "add_credits": "Thêm tín dụng", + "contact_support": "Liên hệ hỗ trợ" } }, "codeActions": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 65a6fac5ac..1ff8d330cf 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -266,11 +266,20 @@ "invalid_token_received": "Zoo Code: 收到无效的认证 token。", "token_verification_failed": "Zoo Code: Token 验证失败。", "invalid_token": "Zoo Code: 无效的 token。", - "could_not_verify_token": "Zoo Code: 无法验证 token。" + "could_not_verify_token": "Zoo Code: 无法验证 token。", + "session_expired": "Zoo Code 会话已过期。请重新登录以继续使用 Zoo Gateway。", + "out_of_credits": "Zoo Gateway:积分不足。请添加积分以继续。", + "account_unavailable": "Zoo Gateway:你的账户当前不可用。请联系支持。", + "budget_exceeded": "Zoo Gateway:已达到使用预算。请等待预算重置或充值。" }, "info": { "connected": "Zoo Code: 连接成功!你现在可以使用 Zoo Code 作为 AI 提供商。", "disconnected": "Zoo Code: 已成功断开连接。" + }, + "buttons": { + "sign_in": "登录", + "add_credits": "添加积分", + "contact_support": "联系支持" } }, "codeActions": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index ba9af0cf53..74e661e280 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -261,11 +261,20 @@ "invalid_token_received": "Zoo Code: 收到無效的認證 token。", "token_verification_failed": "Zoo Code: Token 驗證失敗。", "invalid_token": "Zoo Code: 無效的 token。", - "could_not_verify_token": "Zoo Code: 無法驗證 token。" + "could_not_verify_token": "Zoo Code: 無法驗證 token。", + "session_expired": "Zoo Code 工作階段已過期。請重新登入以繼續使用 Zoo Gateway。", + "out_of_credits": "Zoo Gateway:點數不足。請新增點數以繼續。", + "account_unavailable": "Zoo Gateway:您的帳戶目前無法使用。請聯絡支援。", + "budget_exceeded": "Zoo Gateway:已達使用預算。請等待預算重置或加值。" }, "info": { "connected": "Zoo Code: 連線成功!你現在可以使用 Zoo Code 作為 AI 提供商。", "disconnected": "Zoo Code: 已成功中斷連線。" + }, + "buttons": { + "sign_in": "登入", + "add_credits": "新增點數", + "contact_support": "聯絡支援" } }, "codeActions": { From 7e54cf11636155836daaa1949bd04a632d61eb1e Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Thu, 28 May 2026 15:28:38 -0600 Subject: [PATCH 18/20] test(zoo-gateway): cover surfaceGatewayApiError UX branches for codecov patch Adds vscode + i18n mocks and asserts the 401/402/403/429 paths in surfaceGatewayApiError: token clear + sign-in URL on 401, add-credits URL on 402 and budget-coded 429, support URL on 403, no-op on 429 without a budget code or on errors without a status. Also verifies the helper still runs before completePrompt rewraps the upstream error. Co-authored-by: Cursor --- .../providers/__tests__/zoo-gateway.spec.ts | 147 +++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/zoo-gateway.spec.ts b/src/api/providers/__tests__/zoo-gateway.spec.ts index bfc02d79aa..77bfd761d9 100644 --- a/src/api/providers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/__tests__/zoo-gateway.spec.ts @@ -1,6 +1,19 @@ // npx vitest run src/api/providers/__tests__/zoo-gateway.spec.ts -vitest.mock("vscode", () => ({})) +const { showErrorMessage, openExternal } = vitest.hoisted(() => ({ + showErrorMessage: vitest.fn(async () => undefined as string | undefined), + openExternal: vitest.fn(async () => true), +})) + +vitest.mock("vscode", () => ({ + window: { showErrorMessage }, + env: { openExternal, uriScheme: "vscode", appName: "VS Code" }, + Uri: { parse: (value: string) => ({ toString: () => value }) }, +})) + +vitest.mock("../../../i18n", () => ({ + t: (key: string) => key, +})) import OpenAI from "openai" @@ -9,6 +22,7 @@ import { zooGatewayDefaultModelId, ZOO_GATEWAY_DEFAULT_TEMPERATURE } from "@roo- import { ZooGatewayHandler } from "../zoo-gateway" import { ApiHandlerOptions } from "../../../shared/api" import { Package } from "../../../shared/package" +import { clearZooCodeToken } from "../../../services/zoo-code-auth" vitest.mock("openai") vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) })) @@ -78,9 +92,32 @@ describe("ZooGatewayHandler", () => { beforeEach(() => { vitest.clearAllMocks() mockCreate.mockClear() + showErrorMessage.mockReset() + showErrorMessage.mockResolvedValue(undefined) + openExternal.mockReset() + openExternal.mockResolvedValue(true) mockOpenAIClient() }) + function makeApiError(status: number, options: { code?: string; message?: string } = {}) { + const err = new Error(options.message ?? `HTTP ${status}`) as Error & { + status: number + code?: string + } + err.status = status + if (options.code) err.code = options.code + return err + } + + async function drainCreateMessage(handler: ZooGatewayHandler) { + const stream = handler.createMessage("system", [{ role: "user", content: "hi" }]) + const out: unknown[] = [] + for await (const chunk of stream) { + out.push(chunk) + } + return out + } + describe("constructor", () => { it("requires authentication before constructing the client", () => { expect(() => new ZooGatewayHandler({})).toThrow( @@ -323,4 +360,112 @@ describe("ZooGatewayHandler", () => { await expect(handler.completePrompt("Test")).resolves.toBe("") }) }) + + describe("surfaceGatewayApiError", () => { + it("clears the cached token and offers re-sign-in on 401", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(401) + }) + showErrorMessage.mockResolvedValueOnce("common:zooAuth.buttons.sign_in") + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(clearZooCodeToken).toHaveBeenCalledTimes(1) + expect(showErrorMessage).toHaveBeenCalledWith( + "common:zooAuth.errors.session_expired", + "common:zooAuth.buttons.sign_in", + ) + expect(openExternal).toHaveBeenCalledTimes(1) + }) + + it("does not open a URL on 401 when the user dismisses the prompt", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(401) + }) + showErrorMessage.mockResolvedValueOnce(undefined) + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(clearZooCodeToken).toHaveBeenCalledTimes(1) + expect(openExternal).not.toHaveBeenCalled() + }) + + it("prompts to add credits on 402", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(402) + }) + showErrorMessage.mockResolvedValueOnce("common:zooAuth.buttons.add_credits") + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(clearZooCodeToken).not.toHaveBeenCalled() + expect(showErrorMessage).toHaveBeenCalledWith( + "common:zooAuth.errors.out_of_credits", + "common:zooAuth.buttons.add_credits", + ) + expect(openExternal).toHaveBeenCalledTimes(1) + }) + + it("shows the budget message on 429 with a budget code", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(429, { code: "monthly_budget_exceeded" }) + }) + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(showErrorMessage).toHaveBeenCalledWith( + "common:zooAuth.errors.budget_exceeded", + "common:zooAuth.buttons.add_credits", + ) + }) + + it("does not surface a notification on 429 without a budget code", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(429, { code: "rate_limited" }) + }) + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(showErrorMessage).not.toHaveBeenCalled() + }) + + it("offers contact support on 403", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(403) + }) + showErrorMessage.mockResolvedValueOnce("common:zooAuth.buttons.contact_support") + + await expect(drainCreateMessage(handler)).rejects.toThrow() + expect(showErrorMessage).toHaveBeenCalledWith( + "common:zooAuth.errors.account_unavailable", + "common:zooAuth.buttons.contact_support", + ) + expect(openExternal).toHaveBeenCalledTimes(1) + }) + + it("ignores errors without an HTTP status", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw new Error("network down") + }) + + await expect(drainCreateMessage(handler)).rejects.toThrow("network down") + expect(showErrorMessage).not.toHaveBeenCalled() + expect(clearZooCodeToken).not.toHaveBeenCalled() + }) + + it("surfaces the gateway error then wraps the message in completePrompt", async () => { + const handler = new ZooGatewayHandler(mockOptions) + mockCreate.mockImplementation(() => { + throw makeApiError(402, { message: "out of credits" }) + }) + + await expect(handler.completePrompt("ping")).rejects.toThrow("Zoo Gateway completion error: out of credits") + expect(showErrorMessage).toHaveBeenCalledWith( + "common:zooAuth.errors.out_of_credits", + "common:zooAuth.buttons.add_credits", + ) + }) + }) }) From 9fe486782bee1ce9becbff3153026b3c852694ab Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 07:34:18 -0600 Subject: [PATCH 19/20] docs(privacy): align observability retention with website policy Co-authored-by: Cursor --- PRIVACY.md | 19 ++++++++++++------- src/services/zoo-telemetry.ts | 22 +++++----------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 25db3a8813..390d981efb 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -40,13 +40,18 @@ go—and, importantly, where they don't. We retain telemetry only as long as needed for product analytics and debugging. Telemetry does **not** collect your code or AI prompts, and you can opt out at any time through the settings. -- **Zoo Code Observability (Authenticated Subscribers Only):** If you sign in to - Zoo Code and have an active subscription, Zoo Code will send LLM usage - telemetry to the Zoo Code backend (zoocode.dev). This includes task ID, AI - provider name, model name, token counts (input/output/cache), and estimated - cost. This data is linked to your authenticated Zoo Code account. You can stop - this collection at any time by signing out via the Zoo Code badge in the chat - area. +- **Zoo Code Observability (All Authenticated Users):** If you sign in to + Zoo Code, Zoo Code will send LLM usage telemetry to the Zoo Code backend + (zoocode.dev). This includes task ID, AI provider name, model name, token + counts (input/output/cache), and estimated cost. This data is linked to your + authenticated Zoo Code account and is retained for up to 90 days as + metadata-only API request logs, as described in the + [zoocode.dev Privacy Policy](https://www.zoocode.dev/legal/privacy). Free + plan users can view their telemetry in the dashboard for the most recent 7 + days; Pro and higher plan users can view the full 90-day window. You can + stop this collection at any time by signing out via the Zoo Code badge in + the chat area, and you may request deletion of your data at any time per + the privacy policy. - **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Zoo Code makes a secure API call to Zoo Code's backend servers to retrieve listing information. These diff --git a/src/services/zoo-telemetry.ts b/src/services/zoo-telemetry.ts index b181ff6d5b..6b6f226813 100644 --- a/src/services/zoo-telemetry.ts +++ b/src/services/zoo-telemetry.ts @@ -1,9 +1,4 @@ -import { - getCachedZooCodeToken, - getZooCodeBaseUrl, - getCachedSubscriptionStatus, - checkSubscriptionStatus, -} from "./zoo-code-auth" +import { getCachedZooCodeToken, getZooCodeBaseUrl } from "./zoo-code-auth" import { Package } from "../shared/package" export type LlmTelemetryPayload = { @@ -22,7 +17,10 @@ export type LlmTelemetryPayload = { /** * Send LLM telemetry to the Zoo Code observability backend. * This is a fire-and-forget operation that silently fails on error. - * Only sends telemetry for authenticated users with active subscriptions. + * Sends telemetry for all authenticated users — free and paid alike. + * Server-side retention follows the zoocode.dev privacy policy (metadata-only + * API request logs are kept up to 90 days). Dashboard visibility is plan-gated + * (7 days for Free; full window for Pro and higher). */ export async function sendLlmTelemetry(payload: LlmTelemetryPayload): Promise { const token = getCachedZooCodeToken() @@ -30,16 +28,6 @@ export async function sendLlmTelemetry(payload: LlmTelemetryPayload): Promise "unknown" as const) - } - - if (status !== "active") { - return - } - const baseUrl = getZooCodeBaseUrl() const body = { From 91f52ae772e97a1c195c0904dbd40932cda9e97f Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 27 May 2026 08:13:19 -0600 Subject: [PATCH 20/20] test(zoo-telemetry): align with privacy policy update - drop subscription gating expectation Co-authored-by: Cursor --- src/services/__tests__/zoo-telemetry.test.ts | 37 +++++++++----------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/services/__tests__/zoo-telemetry.test.ts b/src/services/__tests__/zoo-telemetry.test.ts index 5f5ace80de..d0d7ad44ed 100644 --- a/src/services/__tests__/zoo-telemetry.test.ts +++ b/src/services/__tests__/zoo-telemetry.test.ts @@ -1,20 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" -const { - mockCheckSubscriptionStatus, - mockGetCachedSubscriptionStatus, - mockGetCachedZooCodeToken, - mockGetZooCodeBaseUrl, -} = vi.hoisted(() => ({ - mockCheckSubscriptionStatus: vi.fn(), - mockGetCachedSubscriptionStatus: vi.fn(), +const { mockGetCachedZooCodeToken, mockGetZooCodeBaseUrl } = vi.hoisted(() => ({ mockGetCachedZooCodeToken: vi.fn(), mockGetZooCodeBaseUrl: vi.fn(), })) vi.mock("../zoo-code-auth", () => ({ - checkSubscriptionStatus: mockCheckSubscriptionStatus, - getCachedSubscriptionStatus: mockGetCachedSubscriptionStatus, getCachedZooCodeToken: mockGetCachedZooCodeToken, getZooCodeBaseUrl: mockGetZooCodeBaseUrl, })) @@ -52,21 +43,29 @@ describe("sendLlmTelemetry", () => { expect(global.fetch).not.toHaveBeenCalled() }) - it("refreshes an unknown subscription status before sending", async () => { - mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("unknown") - mockCheckSubscriptionStatus.mockResolvedValue("inactive") - global.fetch = vi.fn() + it("sends telemetry for authenticated users regardless of subscription tier", async () => { + // Privacy policy alignment: server-side retention (up to 90 days) and + // plan-gated dashboard visibility are enforced on zoocode.dev. The + // extension no longer filters telemetry by subscription status, so any + // authenticated user — free or paid — should reach the fetch call. + mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_free_user_token") + global.fetch = vi.fn().mockResolvedValue({ ok: true }) await sendLlmTelemetry(payload) - expect(mockCheckSubscriptionStatus).toHaveBeenCalled() - expect(global.fetch).not.toHaveBeenCalled() + expect(global.fetch).toHaveBeenCalledWith( + "https://www.zoocode.dev/api/observability/events", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: "Bearer zoo_ext_free_user_token", + }), + }), + ) }) it("fires the observability request without waiting for it to settle", async () => { mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("active") let resolveFetch: ((value: unknown) => void) | undefined global.fetch = vi.fn( @@ -104,7 +103,6 @@ describe("sendLlmTelemetry", () => { it("sends cancelled status when provided in payload", async () => { mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("active") global.fetch = vi.fn().mockResolvedValue({ ok: true }) @@ -119,7 +117,6 @@ describe("sendLlmTelemetry", () => { it("defaults to completed status when not provided", async () => { mockGetCachedZooCodeToken.mockReturnValue("zoo_ext_test_token") - mockGetCachedSubscriptionStatus.mockReturnValue("active") global.fetch = vi.fn().mockResolvedValue({ ok: true })