From 4f77882c72d19df45ab3c53a4c94b3d6f5794de8 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 15:04:24 -0600 Subject: [PATCH 01/26] first pass --- packages/types/src/provider-settings.ts | 16 +- 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 | 4 + src/api/providers/fetchers/zoo-gateway.ts | 112 +++++++++++++ src/api/providers/index.ts | 1 + src/api/providers/zoo-gateway.ts | 171 ++++++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 1 + src/shared/api.ts | 1 + 10 files changed, 335 insertions(+), 2 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 2de2965d92..bbdf0da9dc 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -37,6 +37,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 export const dynamicProviders = [ "openrouter", "vercel-ai-gateway", + "zoo-gateway", "litellm", "requesty", "roo", @@ -391,6 +392,12 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({ vercelAiGatewayModelId: z.string().optional(), }) +const zooGatewaySchema = baseProviderSettingsSchema.extend({ + zooGatewayApiKey: z.string().optional(), + zooGatewayModelId: z.string().optional(), + zooGatewayBaseUrl: z.string().optional(), +}) + const basetenSchema = apiModelIdProviderModelSchema.extend({ basetenApiKey: z.string().optional(), }) @@ -429,6 +436,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), + zooGatewaySchema.merge(z.object({ apiProvider: z.literal("zoo-gateway") })), defaultSchema, ]) @@ -463,6 +471,7 @@ export const providerSettingsSchema = z.object({ ...qwenCodeSchema.shape, ...rooSchema.shape, ...vercelAiGatewaySchema.shape, + ...zooGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, }) @@ -493,6 +502,7 @@ export const modelIdKeys = [ "unboundModelId", "litellmModelId", "vercelAiGatewayModelId", + "zooGatewayModelId", ] as const satisfies readonly (keyof ProviderSettings)[] export type ModelIdKey = (typeof modelIdKeys)[number] @@ -538,6 +548,7 @@ export const modelIdKeysByProvider: Record = { fireworks: "apiModelId", roo: "apiModelId", "vercel-ai-gateway": "vercelAiGatewayModelId", + "zoo-gateway": "zooGatewayModelId", } /** @@ -556,10 +567,10 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str return "anthropic" } - // Vercel AI Gateway uses anthropic protocol for anthropic models. + // Vercel AI Gateway, Zoo Gateway, and Roo use anthropic protocol for anthropic models. if ( provider && - ["vercel-ai-gateway", "roo"].includes(provider) && + ["vercel-ai-gateway", "zoo-gateway", "roo"].includes(provider) && modelId && modelId.toLowerCase().startsWith("anthropic/") ) { @@ -655,6 +666,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 6c180d5dda..2cd7c43084 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -25,6 +25,7 @@ export * from "./xai.js" export * from "./vercel-ai-gateway.js" export * from "./zai.js" export * from "./minimax.js" +export * from "./zoo-gateway.js" import { anthropicDefaultModelId } from "./anthropic.js" import { basetenDefaultModelId } from "./baseten.js" @@ -49,6 +50,7 @@ import { xaiDefaultModelId } from "./xai.js" import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js" import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js" import { minimaxDefaultModelId } from "./minimax.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 9e0c407822..ff8f5102a6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -32,6 +32,7 @@ import { ZAiHandler, FireworksHandler, VercelAiGatewayHandler, + ZooGatewayHandler, MiniMaxHandler, BasetenHandler, } from "./providers" @@ -173,6 +174,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 7e85fe7005..20693d133f 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -27,6 +27,7 @@ import { getLMStudioModels } from "./lmstudio" import { getPoeModels } from "./poe" import { getRooModels } from "./roo" import { getDeepSeekModels } from "./deepseek" +import { getZooGatewayModels } from "./zoo-gateway" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -99,6 +100,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise + +/** + * 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 ?? "https://zoocode.dev/api/gateway/v1" + + // Build headers - Zoo Gateway requires authentication + const headers: Record = {} + if (options?.zooGatewayApiKey) { + headers["Authorization"] = `Bearer ${options.zooGatewayApiKey}` + } + + try { + const response = await axios.get(`${baseURL}/models`, { + headers, + }) + 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) { + console.error( + `Error fetching Zoo Gateway models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + } + + 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 41aff953d4..ee1c8622c0 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -26,5 +26,6 @@ export { ZAiHandler } from "./zai" export { FireworksHandler } from "./fireworks" export { RooHandler } from "./roo" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" +export { ZooGatewayHandler } from "./zoo-gateway" export { MiniMaxHandler } from "./minimax" 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..81391cfd13 --- /dev/null +++ b/src/api/providers/zoo-gateway.ts @@ -0,0 +1,171 @@ +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 { 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 ?? "https://zoocode.dev/api/gateway/v1" + + super({ + options, + name: "zoo-gateway", + baseURL, + apiKey: options.zooGatewayApiKey, + 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.zooGatewayApiKey ?? "not-provided", + 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/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index dc029cb7dd..7a80bf7083 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: {}, diff --git a/src/shared/api.ts b/src/shared/api.ts index aa7cb2ed68..0386c43685 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -178,6 +178,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 698ac9dd82997d01c34adc574fabaef4d1fe0d34 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 15:28:18 -0600 Subject: [PATCH 02/26] refactor --- packages/types/src/provider-settings.ts | 2 +- src/api/providers/fetchers/modelCache.ts | 2 +- src/api/providers/fetchers/zoo-gateway.ts | 6 +++--- src/api/providers/zoo-gateway.ts | 4 ++-- src/core/webview/ClineProvider.ts | 25 ++++++++++++++++++++--- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index bbdf0da9dc..b5a170fc59 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -393,7 +393,7 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({ }) const zooGatewaySchema = baseProviderSettingsSchema.extend({ - zooGatewayApiKey: z.string().optional(), + zooSessionToken: z.string().optional(), zooGatewayModelId: z.string().optional(), zooGatewayBaseUrl: z.string().optional(), }) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 20693d133f..ade8cef4ef 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -101,7 +101,7 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise = {} const baseURL = options?.zooGatewayBaseUrl ?? "https://zoocode.dev/api/gateway/v1" - // Build headers - Zoo Gateway requires authentication + // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token const headers: Record = {} - if (options?.zooGatewayApiKey) { - headers["Authorization"] = `Bearer ${options.zooGatewayApiKey}` + if (options?.zooSessionToken) { + headers["Authorization"] = `Bearer ${options.zooSessionToken}` } try { diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index 81391cfd13..f2b7122f38 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -33,7 +33,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio options, name: "zoo-gateway", baseURL, - apiKey: options.zooGatewayApiKey, + apiKey: options.zooSessionToken, modelId: options.zooGatewayModelId, defaultModelId: zooGatewayDefaultModelId, defaultModelInfo: zooGatewayDefaultModelInfo, @@ -53,7 +53,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio // Recreate client with enrichment headers ;(this as any).client = new OpenAI({ baseURL, - apiKey: options.zooGatewayApiKey ?? "not-provided", + apiKey: options.zooSessionToken ?? "not-provided", defaultHeaders: { ...DEFAULT_HEADERS, ...enrichmentHeaders, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ad339f52a4..d782fd2c1c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1682,12 +1682,31 @@ 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. + // Auto-populate the zoo-gateway provider profile with the session token so that + // ZooGatewayHandler can authenticate without any manual user input. + try { + const { apiConfiguration, currentApiConfigName = "default" } = await this.getState() + const profileName = "Zoo Gateway" + const newConfiguration: ProviderSettings = { + ...apiConfiguration, + apiProvider: "zoo-gateway", + zooSessionToken: token, + zooGatewayModelId: apiConfiguration.zooGatewayModelId, + zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, + } + await this.upsertProviderProfile(profileName, newConfiguration) + } catch (error) { + this.log( + `[handleZooCodeCallback] Failed to auto-populate zoo-gateway profile: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + } await this.postStateToWebview() } From 8dec8a9458fe2c95104edeb7e158a5188c4a9911 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 16:28:53 -0600 Subject: [PATCH 03/26] fix missing items --- .../src/components/settings/ApiOptions.tsx | 12 +++ .../src/components/settings/ModelPicker.tsx | 1 + .../src/components/settings/constants.ts | 1 + .../settings/providers/ZooGateway.tsx | 75 +++++++++++++++++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 9 +++ .../src/utils/__tests__/validate.spec.ts | 1 + 7 files changed, 100 insertions(+) create mode 100644 webview-ui/src/components/settings/providers/ZooGateway.tsx diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 8981f8d3b7..0f1caa80ff 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -92,6 +92,7 @@ import { ZAi, Fireworks, VercelAiGateway, + ZooGateway, MiniMax, } from "./providers" @@ -690,6 +691,17 @@ const ApiOptions = ({ /> )} + {selectedProvider === "zoo-gateway" && ( + + )} + {selectedProvider === "fireworks" && ( void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const ZooGateway = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, + simplifySettings, +}: ZooGatewayProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + {/* Zoo Gateway uses zooSessionToken for auth, set automatically on login. + We still expose it here so users can inspect/override it if needed. */} + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 7badb54311..cd2e9e3c1c 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -22,5 +22,6 @@ 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 { Baseten } from "./Baseten" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 4b01d55740..49be0dcac5 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -345,6 +345,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 dcd7a1edbc..50ba912a3c 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": {}, roo: {}, poe: {}, deepseek: {}, From 835c0b889cb19b00b34a8e89c5c3380912292a62 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 17:10:30 -0600 Subject: [PATCH 04/26] misc fix --- src/core/webview/webviewMessageHandler.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 7a80bf7083..ed6b990156 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -965,6 +965,14 @@ export const webviewMessageHandler = async ( }, }, { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, + { + key: "zoo-gateway", + options: { + provider: "zoo-gateway", + apiKey: apiConfiguration.zooSessionToken, + baseUrl: apiConfiguration.zooGatewayBaseUrl, + }, + }, ] // LiteLLM is conditional on baseUrl+apiKey From f59f98f53b7c086527acfbd57f46cd0da13e7510 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 17:31:34 -0600 Subject: [PATCH 05/26] prevent auto provider switch --- src/core/webview/ClineProvider.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d782fd2c1c..fab3d7d204 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1687,10 +1687,15 @@ export class ClineProvider 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. - // Auto-populate the zoo-gateway provider profile with the session token so that + // Save the zoo-gateway provider profile with the session token so that // ZooGatewayHandler can authenticate without any manual user input. + // + // activate: false — do NOT switch the active provider or rebuild the current + // task's API handler. The user must explicitly select Zoo Gateway in settings. + // Passing activate: true (the default) would call updateTaskApiHandlerIfNeeded + // with forceRebuild: true, silently switching providers mid-conversation. try { - const { apiConfiguration, currentApiConfigName = "default" } = await this.getState() + const { apiConfiguration } = await this.getState() const profileName = "Zoo Gateway" const newConfiguration: ProviderSettings = { ...apiConfiguration, @@ -1699,10 +1704,10 @@ export class ClineProvider zooGatewayModelId: apiConfiguration.zooGatewayModelId, zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, } - await this.upsertProviderProfile(profileName, newConfiguration) + await this.upsertProviderProfile(profileName, newConfiguration, false) } catch (error) { this.log( - `[handleZooCodeCallback] Failed to auto-populate zoo-gateway profile: ${ + `[handleZooCodeCallback] Failed to save zoo-gateway profile: ${ error instanceof Error ? error.message : String(error) }`, ) From 7a032a5343eabb438169c9c18db5230f8be75d6d Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 19:17:02 -0600 Subject: [PATCH 06/26] fix --- src/core/webview/webviewMessageHandler.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index ed6b990156..ef6a7d6437 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2441,6 +2441,27 @@ export const webviewMessageHandler = async ( try { const { disconnectZooCode } = await import("../../services/zoo-code-auth") await disconnectZooCode() + + // Also clear the zooSessionToken from the "Zoo Gateway" provider profile + // The token is saved there by handleZooCodeCallback() and persists after sign-out + try { + const profileName = "Zoo Gateway" + if (provider.hasProviderProfileEntry(profileName)) { + const profile = await provider.providerSettingsManager.getProfile({ name: profileName }) + if (profile.zooSessionToken) { + // Clear the token from the profile + const { zooSessionToken: _removed, ...cleanedProfile } = profile + await provider.providerSettingsManager.saveConfig(profileName, cleanedProfile) + provider.log(`[zooCodeSignOut] Cleared zooSessionToken from "${profileName}" 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 0222517d79f992b1689690ac77ac9cbe1b611c0c Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 21:59:04 -0600 Subject: [PATCH 07/26] using correct url --- src/api/providers/fetchers/zoo-gateway.ts | 2 +- src/api/providers/zoo-gateway.ts | 2 +- webview-ui/src/components/settings/providers/ZooGateway.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/providers/fetchers/zoo-gateway.ts b/src/api/providers/fetchers/zoo-gateway.ts index 16e14e91b6..645cf753b7 100644 --- a/src/api/providers/fetchers/zoo-gateway.ts +++ b/src/api/providers/fetchers/zoo-gateway.ts @@ -59,7 +59,7 @@ type ZooGatewayModelsResponse = z.infer export async function getZooGatewayModels(options?: ApiHandlerOptions): Promise> { const models: Record = {} - const baseURL = options?.zooGatewayBaseUrl ?? "https://zoocode.dev/api/gateway/v1" + const baseURL = options?.zooGatewayBaseUrl ?? "https://www.zoocode.dev/api/gateway/v1" // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token const headers: Record = {} diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index f2b7122f38..c52a29dc95 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -27,7 +27,7 @@ interface ZooGatewayUsage extends OpenAI.CompletionUsage { export class ZooGatewayHandler extends RouterProvider implements SingleCompletionHandler { constructor(options: ApiHandlerOptions) { - const baseURL = options.zooGatewayBaseUrl ?? "https://zoocode.dev/api/gateway/v1" + const baseURL = options.zooGatewayBaseUrl ?? "https://www.zoocode.dev/api/gateway/v1" super({ options, diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index 2a9638add7..26d4a2afc6 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -65,7 +65,7 @@ export const ZooGateway = ({ models={routerModels?.["zoo-gateway"] ?? {}} modelIdKey="zooGatewayModelId" serviceName="Zoo Gateway" - serviceUrl="https://zoocode.dev/dashboard" + serviceUrl="https://www.zoocode.dev/dashboard" organizationAllowList={organizationAllowList} errorMessage={modelValidationError} simplifySettings={simplifySettings} From 62e662f5b88e5a1567c635f63bc9edc9e60fea1a Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 23:01:09 -0600 Subject: [PATCH 08/26] ensure authentication before Zoo Gateway usage --- src/api/providers/zoo-gateway.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index c52a29dc95..71ef14cdba 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -29,6 +29,13 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio constructor(options: ApiHandlerOptions) { const baseURL = options.zooGatewayBaseUrl ?? "https://www.zoocode.dev/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", @@ -53,7 +60,7 @@ export class ZooGatewayHandler extends RouterProvider implements SingleCompletio // Recreate client with enrichment headers ;(this as any).client = new OpenAI({ baseURL, - apiKey: options.zooSessionToken ?? "not-provided", + apiKey: options.zooSessionToken, defaultHeaders: { ...DEFAULT_HEADERS, ...enrichmentHeaders, From 5dd8c8969bb96cd6dcaf6b806c01ec134e5c1ac0 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Fri, 15 May 2026 23:50:17 -0600 Subject: [PATCH 09/26] refactor: improve Zoo Gateway profile handling during session token updates --- src/core/webview/ClineProvider.ts | 15 ++++++++++----- src/core/webview/webviewMessageHandler.ts | 21 +++++++++++++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index fab3d7d204..35d7a3ce27 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1690,10 +1690,10 @@ export class ClineProvider // Save the zoo-gateway provider profile with the session token so that // ZooGatewayHandler can authenticate without any manual user input. // - // activate: false — do NOT switch the active provider or rebuild the current - // task's API handler. The user must explicitly select Zoo Gateway in settings. - // Passing activate: true (the default) would call updateTaskApiHandlerIfNeeded - // with forceRebuild: true, silently switching providers mid-conversation. + // 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 profileName = "Zoo Gateway" @@ -1704,7 +1704,12 @@ export class ClineProvider zooGatewayModelId: apiConfiguration.zooGatewayModelId, zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, } - await this.upsertProviderProfile(profileName, newConfiguration, false) + + // Check if Zoo Gateway is the currently active profile + const currentApiConfigName = this.contextProxy.getValues().currentApiConfigName + const isZooGatewayActive = currentApiConfigName === profileName + + await this.upsertProviderProfile(profileName, newConfiguration, isZooGatewayActive) } catch (error) { this.log( `[handleZooCodeCallback] Failed to save zoo-gateway profile: ${ diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index ef6a7d6437..14ff048535 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2451,8 +2451,25 @@ export const webviewMessageHandler = async ( if (profile.zooSessionToken) { // Clear the token from the profile const { zooSessionToken: _removed, ...cleanedProfile } = profile - await provider.providerSettingsManager.saveConfig(profileName, cleanedProfile) - provider.log(`[zooCodeSignOut] Cleared zooSessionToken from "${profileName}" profile`) + + // Check if Zoo Gateway is the currently active profile. + // If so, we must push the cleared profile to the in-memory handler + // using upsertProviderProfile with activate: true. Otherwise the + // current Task's API handler retains the stale token. + const currentApiConfigName = provider.contextProxy.getValues().currentApiConfigName + const isZooGatewayActive = currentApiConfigName === profileName + + if (isZooGatewayActive) { + // Push cleared profile to in-memory handler + await provider.upsertProviderProfile(profileName, cleanedProfile, true) + provider.log( + `[zooCodeSignOut] Cleared zooSessionToken from "${profileName}" profile and updated in-memory handler`, + ) + } else { + // Just persist to disk; in-memory handler is not using Zoo Gateway + await provider.providerSettingsManager.saveConfig(profileName, cleanedProfile) + provider.log(`[zooCodeSignOut] Cleared zooSessionToken from "${profileName}" profile`) + } } } } catch (profileError) { From af52468e58787ba90dea3de71de212c08bd4462b Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Sat, 16 May 2026 00:01:24 -0600 Subject: [PATCH 10/26] refactor: enhance zooSessionToken clearing for all profiles using zoo-gateway provider --- src/core/webview/ClineProvider.ts | 7 +-- src/core/webview/webviewMessageHandler.ts | 59 +++++++++++++---------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 35d7a3ce27..f36ecf9040 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1705,9 +1705,10 @@ export class ClineProvider zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, } - // Check if Zoo Gateway is the currently active profile - const currentApiConfigName = this.contextProxy.getValues().currentApiConfigName - const isZooGatewayActive = currentApiConfigName === profileName + // Check if Zoo Gateway is the currently active profile by apiProvider identity, + // not by profile name (profile names are user-renameable). + const currentSettings = this.contextProxy.getProviderSettings() + const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" await this.upsertProviderProfile(profileName, newConfiguration, isZooGatewayActive) } catch (error) { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 14ff048535..e635b7e764 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2442,33 +2442,40 @@ export const webviewMessageHandler = async ( const { disconnectZooCode } = await import("../../services/zoo-code-auth") await disconnectZooCode() - // Also clear the zooSessionToken from the "Zoo Gateway" provider profile - // The token is saved there by handleZooCodeCallback() and persists after sign-out + // 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 profileName = "Zoo Gateway" - if (provider.hasProviderProfileEntry(profileName)) { - const profile = await provider.providerSettingsManager.getProfile({ name: profileName }) - if (profile.zooSessionToken) { - // Clear the token from the profile - const { zooSessionToken: _removed, ...cleanedProfile } = profile - - // Check if Zoo Gateway is the currently active profile. - // If so, we must push the cleared profile to the in-memory handler - // using upsertProviderProfile with activate: true. Otherwise the - // current Task's API handler retains the stale token. - const currentApiConfigName = provider.contextProxy.getValues().currentApiConfigName - const isZooGatewayActive = currentApiConfigName === profileName - - if (isZooGatewayActive) { - // Push cleared profile to in-memory handler - await provider.upsertProviderProfile(profileName, cleanedProfile, true) - provider.log( - `[zooCodeSignOut] Cleared zooSessionToken from "${profileName}" profile and updated in-memory handler`, - ) - } else { - // Just persist to disk; in-memory handler is not using Zoo Gateway - await provider.providerSettingsManager.saveConfig(profileName, cleanedProfile) - provider.log(`[zooCodeSignOut] Cleared zooSessionToken from "${profileName}" profile`) + 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 }) + if (profile.zooSessionToken) { + // Clear the token from the profile + const { zooSessionToken: _removed, ...cleanedProfile } = profile + + // If this is the currently active profile, push to in-memory handler + // so the current Task's API handler doesn't retain the stale token. + const isThisProfileActive = isZooGatewayActive && currentApiConfigName === entry.name + + if (isThisProfileActive) { + // Push cleared profile to in-memory handler + await provider.upsertProviderProfile(entry.name, cleanedProfile, true) + provider.log( + `[zooCodeSignOut] Cleared zooSessionToken from "${entry.name}" profile and updated in-memory handler`, + ) + } else { + // Just persist to disk; this profile is not currently active + await provider.providerSettingsManager.saveConfig(entry.name, cleanedProfile) + provider.log( + `[zooCodeSignOut] Cleared zooSessionToken from "${entry.name}" profile`, + ) + } } } } From bb357072f879295eac01a352b13ac20bfb4bb651 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Sat, 16 May 2026 00:40:24 -0600 Subject: [PATCH 11/26] refactor: update profile name handling for Zoo Gateway based on active settings --- src/core/webview/ClineProvider.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index f36ecf9040..44581dac34 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1696,7 +1696,18 @@ export class ClineProvider // must explicitly select Zoo Gateway in settings if they want to use it. try { const { apiConfiguration } = await this.getState() - const profileName = "Zoo Gateway" + const currentSettings = this.contextProxy.getProviderSettings() + const currentApiConfigName = this.contextProxy.getValues().currentApiConfigName + + // 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" + + // If Zoo Gateway is currently active, write to the actual active profile name + // (which may have been renamed by the user). Otherwise fall back to the default + // "Zoo Gateway" name to create or update the canonical default profile. + const profileName = isZooGatewayActive && currentApiConfigName ? currentApiConfigName : "Zoo Gateway" + const newConfiguration: ProviderSettings = { ...apiConfiguration, apiProvider: "zoo-gateway", @@ -1705,11 +1716,6 @@ export class ClineProvider zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, } - // Check if Zoo Gateway is the currently active profile by apiProvider identity, - // not by profile name (profile names are user-renameable). - const currentSettings = this.contextProxy.getProviderSettings() - const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" - await this.upsertProviderProfile(profileName, newConfiguration, isZooGatewayActive) } catch (error) { this.log( From 348fb029c46676914cb881baf5b42a4cc82e2e3e Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Sat, 16 May 2026 01:17:13 -0600 Subject: [PATCH 12/26] refactor: add Zoo Gateway sign-in validation and update localization strings --- src/core/webview/ClineProvider.ts | 50 ++++++++++++++----- 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 +- .../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 +- .../src/i18n/locales/zh-CN/settings.json | 3 +- .../src/i18n/locales/zh-TW/settings.json | 3 +- webview-ui/src/utils/validate.ts | 5 ++ 20 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 44581dac34..bb98b40df4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1703,20 +1703,44 @@ export class ClineProvider // not by profile name (profile names are user-renameable). const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" - // If Zoo Gateway is currently active, write to the actual active profile name - // (which may have been renamed by the user). Otherwise fall back to the default - // "Zoo Gateway" name to create or update the canonical default profile. - const profileName = isZooGatewayActive && currentApiConfigName ? currentApiConfigName : "Zoo Gateway" - - const newConfiguration: ProviderSettings = { - ...apiConfiguration, - apiProvider: "zoo-gateway", - zooSessionToken: token, - zooGatewayModelId: apiConfiguration.zooGatewayModelId, - zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, + if (isZooGatewayActive && currentApiConfigName) { + // Zoo Gateway is active: write to the actual active profile name (may have been renamed). + const newConfiguration: ProviderSettings = { + ...apiConfiguration, + apiProvider: "zoo-gateway", + zooSessionToken: token, + zooGatewayModelId: apiConfiguration.zooGatewayModelId, + zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, + } + await this.upsertProviderProfile(currentApiConfigName, newConfiguration, true) + } else { + // Zoo Gateway is not active. Scan all profiles and update every zoo-gateway profile + // so renamed profiles also get the fresh token. Only create "Zoo Gateway" if + // no zoo-gateway profile exists yet. + 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: apiConfiguration.zooGatewayBaseUrl, + } + await this.upsertProviderProfile("Zoo Gateway", newConfiguration, false) + } else { + // Update every existing zoo-gateway profile with the new token. + for (const entry of zooProfiles) { + const existing = await this.providerSettingsManager.getProfile({ name: entry.name }) + const updated: ProviderSettings = { + ...existing, + zooSessionToken: token, + } + await this.providerSettingsManager.saveConfig(entry.name, updated) + } + } } - - await this.upsertProviderProfile(profileName, newConfiguration, isZooGatewayActive) } catch (error) { this.log( `[handleZooCodeCallback] Failed to save zoo-gateway profile: ${ diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 62b4bab050..a742592aa3 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -891,7 +891,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 b5560f1967..bd7d6ffb87 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -891,7 +891,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 4b48d75149..934e6782c5 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -954,7 +954,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 13b2851f4c..17bb33740d 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -891,7 +891,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 70970bcb88..985753f628 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -891,7 +891,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 b13b824d66..990a437f51 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -891,7 +891,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 1dd362bcb8..aea4ed92f1 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -891,7 +891,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 271c616e63..3eef2c4d35 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -891,7 +891,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 d60bfe88eb..1b6e65367e 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -891,7 +891,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 7c3d833a5d..c360431874 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -891,7 +891,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 37751ccafc..f98b9e7a09 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -891,7 +891,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 56c2a58470..0c02e9e333 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -891,7 +891,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 0271b567c3..29fad1fa37 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -891,7 +891,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 31ff10457d..5d5de068a4 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -891,7 +891,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 e9630d780c..69440377d9 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -891,7 +891,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", + "zooGatewaySignIn": "Zoo Gateway'i kullanmak için Zoo Code'a giriş yapmalısın. Kimlik doğrulamak için 'Giriş Yap'a tıkla." }, "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 c49a75bd77..e76f0851d2 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -891,7 +891,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 d565d18eff..d634d7f69c 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -891,7 +891,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 f75bafe184..7d62579639 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -901,7 +901,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..e79ce1e17e 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -123,6 +123,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "zoo-gateway": + if (!apiConfiguration.zooSessionToken) { + return i18next.t("settings:validation.zooGatewaySignIn") + } + break case "baseten": if (!apiConfiguration.basetenApiKey) { return i18next.t("settings:validation.apiKey") From 8a96a15a22279fc7a37974fef667c03e46c5700c Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Mon, 18 May 2026 21:06:33 -0600 Subject: [PATCH 13/26] refactor: update Zoo Code observability to include all authenticated users and adjust telemetry retention policies --- PRIVACY.md | 11 ++++++----- src/core/webview/ClineProvider.ts | 15 ++++++++++++--- src/services/zoo-telemetry.ts | 20 +++----------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 25db3a8813..5af2f8f642 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -40,11 +40,12 @@ 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 +- **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. Free plan users have their telemetry retained + for 7 days; Pro and higher plan users have unlimited retention. You can stop this collection at any time by signing out via the Zoo Code badge in the chat area. - **Marketplace Requests**: When you browse or search the Marketplace for Model diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bb98b40df4..1d9973d7ae 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1699,6 +1699,13 @@ export class ClineProvider 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" @@ -1710,7 +1717,7 @@ export class ClineProvider apiProvider: "zoo-gateway", zooSessionToken: token, zooGatewayModelId: apiConfiguration.zooGatewayModelId, - zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, + zooGatewayBaseUrl: derivedGatewayBaseUrl, } await this.upsertProviderProfile(currentApiConfigName, newConfiguration, true) } else { @@ -1726,16 +1733,18 @@ export class ClineProvider apiProvider: "zoo-gateway", zooSessionToken: token, zooGatewayModelId: apiConfiguration.zooGatewayModelId, - zooGatewayBaseUrl: apiConfiguration.zooGatewayBaseUrl, + zooGatewayBaseUrl: derivedGatewayBaseUrl, } await this.upsertProviderProfile("Zoo Gateway", newConfiguration, false) } else { - // Update every existing zoo-gateway profile with the new token. + // 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 existing = await this.providerSettingsManager.getProfile({ name: entry.name }) const updated: ProviderSettings = { ...existing, zooSessionToken: token, + zooGatewayBaseUrl: derivedGatewayBaseUrl, } await this.providerSettingsManager.saveConfig(entry.name, updated) } diff --git a/src/services/zoo-telemetry.ts b/src/services/zoo-telemetry.ts index b181ff6d5b..2d26a3ec5a 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,8 @@ 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. + * Retention limits (7 days for free, unlimited for Pro) are enforced server-side. */ export async function sendLlmTelemetry(payload: LlmTelemetryPayload): Promise { const token = getCachedZooCodeToken() @@ -30,16 +26,6 @@ export async function sendLlmTelemetry(payload: LlmTelemetryPayload): Promise "unknown" as const) - } - - if (status !== "active") { - return - } - const baseUrl = getZooCodeBaseUrl() const body = { From 3b6d7805055984de7b85bfc91cebfa820de87b56 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Mon, 18 May 2026 21:12:49 -0600 Subject: [PATCH 14/26] refactor: update Zoo Gateway base URL handling to use getZooCodeBaseUrl --- src/api/providers/fetchers/zoo-gateway.ts | 5 ++--- src/api/providers/zoo-gateway.ts | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/providers/fetchers/zoo-gateway.ts b/src/api/providers/fetchers/zoo-gateway.ts index 645cf753b7..82a8d3101b 100644 --- a/src/api/providers/fetchers/zoo-gateway.ts +++ b/src/api/providers/fetchers/zoo-gateway.ts @@ -1,10 +1,9 @@ import axios from "axios" import type { ModelInfo } from "@roo-code/types" -import { VERCEL_AI_GATEWAY_VISION_ONLY_MODELS, VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS } from "@roo-code/types" import type { ApiHandlerOptions } from "../../../shared/api" -import { parseApiPrice } from "../../../shared/cost" +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" @@ -59,7 +58,7 @@ type ZooGatewayModelsResponse = z.infer export async function getZooGatewayModels(options?: ApiHandlerOptions): Promise> { const models: Record = {} - const baseURL = options?.zooGatewayBaseUrl ?? "https://www.zoocode.dev/api/gateway/v1" + const baseURL = options?.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token const headers: Record = {} diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts index 71ef14cdba..dc425ba508 100644 --- a/src/api/providers/zoo-gateway.ts +++ b/src/api/providers/zoo-gateway.ts @@ -9,6 +9,7 @@ import { } 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" @@ -27,7 +28,7 @@ interface ZooGatewayUsage extends OpenAI.CompletionUsage { export class ZooGatewayHandler extends RouterProvider implements SingleCompletionHandler { constructor(options: ApiHandlerOptions) { - const baseURL = options.zooGatewayBaseUrl ?? "https://www.zoocode.dev/api/gateway/v1" + 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 From 71e0fbcc4ccadbd556af92a9098766446e669ca0 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 10:40:07 -0600 Subject: [PATCH 15/26] refactor: update handleZooCodeCallback to persist zooSessionToken across all provider instances --- src/activate/handleUri.ts | 10 ++++++---- src/core/webview/ClineProvider.ts | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/activate/handleUri.ts b/src/activate/handleUri.ts index 523d254bc3..030be9699b 100644 --- a/src/activate/handleUri.ts +++ b/src/activate/handleUri.ts @@ -50,10 +50,12 @@ 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. + const allInstances = ClineProvider.getAllInstances() + await Promise.all(allInstances.map((instance) => instance.handleZooCodeCallback(token))) } } break diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1d9973d7ae..8206c64fa7 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -661,6 +661,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() From 4c14d269c18a3f4055881434a230136d4a780726 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 12:57:30 -0600 Subject: [PATCH 16/26] refactor: add profile seeding for zoo-gateway users with cached auth tokens --- src/core/webview/ClineProvider.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8206c64fa7..75068d28ec 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -896,6 +896,33 @@ 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). + * 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 if any zoo-gateway profile already exists + const allProfiles = await this.providerSettingsManager.listConfig() + const hasZooGatewayProfile = allProfiles.some((p) => p.apiProvider === "zoo-gateway") + if (hasZooGatewayProfile) return + + // User has token but no profile — seed it via the same path as fresh auth + this.log("[ensureZooGatewayProfileSeeded] Seeding zoo-gateway profile for existing auth token") + await this.handleZooCodeCallback(token) } public async createTaskWithHistoryItem( From 9d3966a90bd9a1a0c1debb80aadad24100e71ae8 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 13:43:12 -0600 Subject: [PATCH 17/26] refactor: enhance mock setup in handleUri tests to include getAllInstances --- src/activate/__tests__/handleUri.spec.ts | 42 +++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/activate/__tests__/handleUri.spec.ts b/src/activate/__tests__/handleUri.spec.ts index 187d9eeeba..a5dc0924a8 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() }) From 14f47711bf71bcc5085d0d4e324964e1b264e57b Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 14:41:50 -0600 Subject: [PATCH 18/26] refactor: enhance zoo-gateway token retrieval to support non-active profiles --- src/core/webview/webviewMessageHandler.ts | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e635b7e764..dd46fdd76f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -946,6 +946,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" } }, @@ -969,8 +991,8 @@ export const webviewMessageHandler = async ( key: "zoo-gateway", options: { provider: "zoo-gateway", - apiKey: apiConfiguration.zooSessionToken, - baseUrl: apiConfiguration.zooGatewayBaseUrl, + apiKey: zooGatewayToken, + baseUrl: zooGatewayBaseUrl, }, }, ] From a0e95ccb2ac96f5f9f5aaa5e9528ad5c4ca8e252 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 15:58:12 -0600 Subject: [PATCH 19/26] refactor: enhance ensureZooGatewayProfileSeeded to handle profiles without tokens --- src/core/webview/ClineProvider.ts | 32 +++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 75068d28ec..24d0d84224 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -907,7 +907,8 @@ export class ClineProvider /** * 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). + * 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 { @@ -915,13 +916,32 @@ export class ClineProvider const token = getCachedZooCodeToken() if (!token) return - // Check if any zoo-gateway profile already exists + // Check if a zoo-gateway profile exists AND has a token. A profile may exist but be + // empty (e.g., imported settings without credentials, or auth completed while no + // provider instance was open). In that case, we still need to write the token. const allProfiles = await this.providerSettingsManager.listConfig() - const hasZooGatewayProfile = allProfiles.some((p) => p.apiProvider === "zoo-gateway") - if (hasZooGatewayProfile) return + const zooGatewayProfile = allProfiles.find((p) => p.apiProvider === "zoo-gateway") - // User has token but no profile — seed it via the same path as fresh auth - this.log("[ensureZooGatewayProfileSeeded] Seeding zoo-gateway profile for existing auth token") + if (zooGatewayProfile) { + // Profile exists — check if it has a token + try { + const fullProfile = await this.providerSettingsManager.getProfile({ name: zooGatewayProfile.name }) + if (fullProfile.zooSessionToken) { + // Profile exists and has a token — nothing to do + return + } + this.log( + "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token", + ) + } catch { + // Profile lookup failed — proceed to seed + this.log("[ensureZooGatewayProfileSeeded] Failed to read existing profile, will re-seed") + } + } else { + this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") + } + + // User has token but either no profile or profile without token — seed it await this.handleZooCodeCallback(token) } From 17f64676175a1b88242fa9eb35c5a12b34922595 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 16:16:05 -0600 Subject: [PATCH 20/26] refactor: enhance ensureZooGatewayProfileSeeded to validate current token against existing profile --- src/core/webview/ClineProvider.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 24d0d84224..6ee21260a5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -916,22 +916,26 @@ export class ClineProvider const token = getCachedZooCodeToken() if (!token) return - // Check if a zoo-gateway profile exists AND has a token. A profile may exist but be - // empty (e.g., imported settings without credentials, or auth completed while no - // provider instance was open). In that case, we still need to write the token. + // Check if a zoo-gateway profile exists AND has the CURRENT token. A profile may exist but: + // - be empty (imported settings without credentials) + // - have a stale token (user re-authenticated while no provider instance was open) + // In either case, we need to write the fresh token. const allProfiles = await this.providerSettingsManager.listConfig() const zooGatewayProfile = allProfiles.find((p) => p.apiProvider === "zoo-gateway") if (zooGatewayProfile) { - // Profile exists — check if it has a token + // Profile exists — check if it has the CURRENT token (not just any token) try { const fullProfile = await this.providerSettingsManager.getProfile({ name: zooGatewayProfile.name }) - if (fullProfile.zooSessionToken) { - // Profile exists and has a token — nothing to do + if (fullProfile.zooSessionToken === token) { + // Profile exists and has the current token — nothing to do return } + // Token mismatch or missing — log and proceed to update this.log( - "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token", + fullProfile.zooSessionToken + ? "[ensureZooGatewayProfileSeeded] Token mismatch (stale session?), updating with current token" + : "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token", ) } catch { // Profile lookup failed — proceed to seed @@ -941,7 +945,7 @@ export class ClineProvider this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") } - // User has token but either no profile or profile without token — seed it + // User has token but either no profile, profile without token, or stale token — seed it await this.handleZooCodeCallback(token) } From 8d86f5582d1e918db969c9df4e8e3f4df475a712 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 19:40:59 -0600 Subject: [PATCH 21/26] refactor: update zoo-gateway profiles to synchronize tokens across all instances --- src/core/webview/ClineProvider.ts | 51 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6ee21260a5..7da66ffd29 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1765,42 +1765,41 @@ export class ClineProvider // not by profile name (profile names are user-renameable). const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" - if (isZooGatewayActive && currentApiConfigName) { - // Zoo Gateway is active: write to the actual active profile name (may have been renamed). + // 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 = { - ...apiConfiguration, apiProvider: "zoo-gateway", zooSessionToken: token, zooGatewayModelId: apiConfiguration.zooGatewayModelId, zooGatewayBaseUrl: derivedGatewayBaseUrl, } - await this.upsertProviderProfile(currentApiConfigName, newConfiguration, true) + // 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 { - // Zoo Gateway is not active. Scan all profiles and update every zoo-gateway profile - // so renamed profiles also get the fresh token. Only create "Zoo Gateway" if - // no zoo-gateway profile exists yet. - 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", + // 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, - zooGatewayModelId: apiConfiguration.zooGatewayModelId, zooGatewayBaseUrl: derivedGatewayBaseUrl, } - await this.upsertProviderProfile("Zoo Gateway", newConfiguration, false) - } 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 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) } } From 24e0f75cbadb7b3361f91a47bdcbd7fdd90a7542 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 21:39:15 -0600 Subject: [PATCH 22/26] refactor: add zoo-gateway to mock models in ClineProvider and webviewMessageHandler tests --- src/core/webview/__tests__/ClineProvider.spec.ts | 10 +++++++--- .../webview/__tests__/webviewMessageHandler.spec.ts | 11 ++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index df8858ec78..bbb55c3e15 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2506,11 +2506,12 @@ describe("ClineProvider - Router Models", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, roo: {}, litellm: mockModels, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, poe: {}, deepseek: {}, }, @@ -2542,6 +2543,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" }) @@ -2553,11 +2555,12 @@ describe("ClineProvider - Router Models", () => { openrouter: mockModels, requesty: {}, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, roo: {}, ollama: {}, lmstudio: {}, litellm: {}, - "vercel-ai-gateway": mockModels, poe: {}, deepseek: {}, }, @@ -2649,11 +2652,12 @@ describe("ClineProvider - Router Models", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, roo: {}, litellm: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, poe: {}, deepseek: {}, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index b68b616ea9..8da15a0450 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -365,11 +365,12 @@ describe("webviewMessageHandler - requestRouterModels", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, litellm: mockModels, roo: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, poe: {}, deepseek: {}, }, @@ -452,11 +453,12 @@ describe("webviewMessageHandler - requestRouterModels", () => { openrouter: mockModels, requesty: mockModels, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, roo: {}, litellm: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, poe: {}, deepseek: {}, }, @@ -480,6 +482,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 +511,12 @@ describe("webviewMessageHandler - requestRouterModels", () => { openrouter: mockModels, requesty: {}, unbound: mockModels, + "vercel-ai-gateway": mockModels, + "zoo-gateway": mockModels, roo: {}, litellm: {}, ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, poe: {}, deepseek: {}, }, @@ -527,6 +531,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, { From cd3057789acd47ebfb37126bd61872867e2e4aa1 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Tue, 19 May 2026 22:54:29 -0600 Subject: [PATCH 23/26] refactor: enhance zoo-gateway profile token validation and seeding logic --- src/core/webview/ClineProvider.ts | 55 +++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 56c69c23d1..4983ae3a7d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -875,36 +875,43 @@ export class ClineProvider const token = getCachedZooCodeToken() if (!token) return - // Check if a zoo-gateway profile exists AND has the CURRENT token. A profile may exist but: - // - be empty (imported settings without credentials) - // - have a stale token (user re-authenticated while no provider instance was open) - // In either case, we need to write the fresh token. + // 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 zooGatewayProfile = allProfiles.find((p) => p.apiProvider === "zoo-gateway") + const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") - if (zooGatewayProfile) { - // Profile exists — check if it has the CURRENT token (not just any token) - try { - const fullProfile = await this.providerSettingsManager.getProfile({ name: zooGatewayProfile.name }) - if (fullProfile.zooSessionToken === token) { - // Profile exists and has the current token — nothing to do - return + 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 } - // Token mismatch or missing — log and proceed to update - 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", - ) - } catch { - // Profile lookup failed — proceed to seed - this.log("[ensureZooGatewayProfileSeeded] Failed to read existing profile, will re-seed") } - } else { - this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") + + if (allUpToDate) { + // All profiles have the current token — nothing to do + return + } } - // User has token but either no profile, profile without token, or stale token — seed it + // User has token but either no profile, some profiles without token, or stale tokens — seed all await this.handleZooCodeCallback(token) } From e1ee061e45005f97ec612cc125b3f2ba536c1560 Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 20 May 2026 05:57:10 -0600 Subject: [PATCH 24/26] refactor: simplify ZooGateway component by removing unused input handling and updating session token display --- .../settings/providers/ZooGateway.tsx | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index 26d4a2afc6..3d81379ae8 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -1,4 +1,3 @@ -import { useCallback } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { @@ -10,7 +9,6 @@ import { import { useAppTranslation } from "@src/i18n/TranslationContext" -import { inputEventTransform } from "../transforms" import { ModelPicker } from "../ModelPicker" type ZooGatewayProps = { @@ -32,31 +30,23 @@ export const ZooGateway = ({ }: ZooGatewayProps) => { const { t } = useAppTranslation() - const handleInputChange = useCallback( - ( - field: K, - transform: (event: E) => ProviderSettings[K] = inputEventTransform, - ) => - (event: E | Event) => { - setApiConfigurationField(field, transform(event as E)) - }, - [setApiConfigurationField], - ) - return ( <> - {/* Zoo Gateway uses zooSessionToken for auth, set automatically on login. - We still expose it here so users can inspect/override it if needed. */} + {/* Zoo Gateway auth is managed exclusively through the "Sign in with Zoo Code" + OAuth flow — the token is set automatically and must not be editable by users. + Showing the field as read-only lets users confirm they are signed in. */}
- {t("settings:providers.apiKeyStorageNotice")} + {apiConfiguration?.zooSessionToken + ? "Signed in via Zoo Code" + : "Sign in via the Zoo Code button to authenticate"}
Date: Wed, 20 May 2026 22:12:19 -0600 Subject: [PATCH 25/26] fix auth in settings for zoo gateway --- .../src/components/settings/ApiOptions.tsx | 12 ++++- .../settings/providers/ZooGateway.tsx | 52 +++++++++++++------ .../welcome/WelcomeViewProvider.tsx | 9 ++-- webview-ui/src/i18n/locales/ca/settings.json | 7 +++ webview-ui/src/i18n/locales/de/settings.json | 7 +++ webview-ui/src/i18n/locales/en/settings.json | 7 +++ webview-ui/src/i18n/locales/es/settings.json | 7 +++ webview-ui/src/i18n/locales/fr/settings.json | 7 +++ webview-ui/src/i18n/locales/hi/settings.json | 7 +++ webview-ui/src/i18n/locales/id/settings.json | 7 +++ webview-ui/src/i18n/locales/it/settings.json | 7 +++ webview-ui/src/i18n/locales/ja/settings.json | 7 +++ webview-ui/src/i18n/locales/ko/settings.json | 7 +++ webview-ui/src/i18n/locales/nl/settings.json | 7 +++ webview-ui/src/i18n/locales/pl/settings.json | 7 +++ .../src/i18n/locales/pt-BR/settings.json | 7 +++ webview-ui/src/i18n/locales/ru/settings.json | 7 +++ webview-ui/src/i18n/locales/tr/settings.json | 7 +++ webview-ui/src/i18n/locales/vi/settings.json | 7 +++ .../src/i18n/locales/zh-CN/settings.json | 7 +++ .../src/i18n/locales/zh-TW/settings.json | 7 +++ webview-ui/src/utils/validate.ts | 13 +++-- 22 files changed, 186 insertions(+), 26 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index cd07e7be08..e63f62744a 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -134,7 +134,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 || {} @@ -273,9 +273,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) => { diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index 3d81379ae8..a08ff73a23 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -1,5 +1,3 @@ -import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" - import { type ProviderSettings, type OrganizationAllowList, @@ -7,6 +5,8 @@ import { 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" @@ -29,24 +29,42 @@ export const ZooGateway = ({ simplifySettings, }: ZooGatewayProps) => { const { t } = useAppTranslation() + const { zooCodeIsAuthenticated, zooCodeUserEmail, zooCodeUserName, zooCodeBaseUrl, uriScheme, deviceName } = + useExtensionState() + + const authUrl = getZooCodeAuthUrl(uriScheme, zooCodeBaseUrl, deviceName) return ( <> - {/* Zoo Gateway auth is managed exclusively through the "Sign in with Zoo Code" - OAuth flow — the token is set automatically and must not be editable by users. - Showing the field as read-only lets users confirm they are signed in. */} - - - -
- {apiConfiguration?.zooSessionToken - ? "Signed in via Zoo Code" - : "Sign in via the Zoo Code button to authenticate"} + {/* 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")} + +
+ )}
{ - 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) @@ -36,7 +37,9 @@ const WelcomeViewProvider = () => { return } - const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined + const error = apiConfiguration + ? validateApiConfiguration(apiConfiguration, undefined, undefined, zooCodeIsAuthenticated) + : undefined if (error) { setErrorMessage(error) @@ -45,7 +48,7 @@ const WelcomeViewProvider = () => { setErrorMessage(undefined) vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) - }, [showProviderSetup, apiConfiguration, currentApiConfigName]) + }, [showProviderSetup, apiConfiguration, currentApiConfigName, zooCodeIsAuthenticated]) const handleBackToLanding = useCallback(() => { setShowProviderSetup(false) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 117125bc1f..71cf946a4f 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 37e0a2a6e2..b2b694db27 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index f516eeda98..8b75697591 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -545,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": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 1239b95d17..524d0b41c8 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index b3e334e174..96ab1ac4de 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 849042e7ad..12c6367959 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -557,6 +557,13 @@ "placeholder": "डिफ़ॉल्ट: claude", "maxTokensLabel": "अधिकतम आउटपुट टोकन", "maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।" + }, + "zooGateway": { + "account": "Zoo Code खाता", + "signInButton": "Zoo Code में साइन इन करें", + "signInDescription": "अपने खाते के साथ Zoo Gateway का उपयोग करने के लिए साइन इन करें", + "authenticated": "प्रमाणित", + "authenticatedAs": "{{name}} के रूप में प्रमाणित" } }, "checkpoints": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 962507849c..4008b046bf 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index e17f87ae6e..4c2b1a5e98 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index e8de1c5c77..c5bfce6ea9 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -557,6 +557,13 @@ "placeholder": "デフォルト:claude", "maxTokensLabel": "最大出力トークン", "maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。" + }, + "zooGateway": { + "account": "Zoo Code アカウント", + "signInButton": "Zoo Code にサインイン", + "signInDescription": "Zoo Gateway をアカウントで使用するにはサインインしてください", + "authenticated": "認証済み", + "authenticatedAs": "{{name}} として認証済み" } }, "checkpoints": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 44dee60e11..c0432d9165 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -557,6 +557,13 @@ "placeholder": "기본값: claude", "maxTokensLabel": "최대 출력 토큰", "maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다." + }, + "zooGateway": { + "account": "Zoo Code 계정", + "signInButton": "Zoo Code에 로그인", + "signInDescription": "계정으로 Zoo Gateway를 사용하려면 로그인하세요", + "authenticated": "인증됨", + "authenticatedAs": "{{name}}(으)로 인증됨" } }, "checkpoints": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index f47a203e99..b022717a94 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bb4c7027a2..460a8753b6 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 17c005d091..e02d741d5d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index e08e354416..f5473bb792 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -557,6 +557,13 @@ "placeholder": "По умолчанию: claude", "maxTokensLabel": "Макс. выходных токенов", "maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000." + }, + "zooGateway": { + "account": "Учетная запись Zoo Code", + "signInButton": "Войти в Zoo Code", + "signInDescription": "Войдите, чтобы использовать Zoo Gateway с вашей учетной записью", + "authenticated": "Аутентифицирован", + "authenticatedAs": "Аутентифицирован как {{name}}" } }, "checkpoints": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 902b09c617..530ece1af9 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index dee434abac..0384944ce1 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 1d9f3bc559..0d4908ef83 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -557,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": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index ff7d2e691c..1b42cfab41 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -567,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": { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index e79ce1e17e..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) { @@ -124,7 +128,7 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri } break case "zoo-gateway": - if (!apiConfiguration.zooSessionToken) { + if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { return i18next.t("settings:validation.zooGatewaySignIn") } break @@ -287,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 From e5ae0b6d6698edee211fffe04d70d267e6a5832d Mon Sep 17 00:00:00 2001 From: James Mtendamema Date: Wed, 20 May 2026 22:50:28 -0600 Subject: [PATCH 26/26] fix: prevent caching of models for zoo-gateway and update service URL in settings --- src/api/providers/fetchers/modelCache.ts | 10 +++++++--- .../src/components/settings/providers/ZooGateway.tsx | 2 +- webview-ui/src/i18n/locales/ca/settings.json | 2 +- webview-ui/src/i18n/locales/de/settings.json | 2 +- webview-ui/src/i18n/locales/en/settings.json | 2 +- webview-ui/src/i18n/locales/es/settings.json | 2 +- webview-ui/src/i18n/locales/fr/settings.json | 2 +- webview-ui/src/i18n/locales/hi/settings.json | 2 +- webview-ui/src/i18n/locales/id/settings.json | 2 +- webview-ui/src/i18n/locales/it/settings.json | 2 +- webview-ui/src/i18n/locales/ja/settings.json | 2 +- webview-ui/src/i18n/locales/ko/settings.json | 2 +- webview-ui/src/i18n/locales/nl/settings.json | 2 +- webview-ui/src/i18n/locales/pl/settings.json | 2 +- webview-ui/src/i18n/locales/pt-BR/settings.json | 2 +- webview-ui/src/i18n/locales/ru/settings.json | 2 +- webview-ui/src/i18n/locales/tr/settings.json | 2 +- webview-ui/src/i18n/locales/vi/settings.json | 2 +- webview-ui/src/i18n/locales/zh-CN/settings.json | 2 +- webview-ui/src/i18n/locales/zh-TW/settings.json | 2 +- 20 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index ade8cef4ef..78e0cf6d80 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -127,7 +127,10 @@ 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 @@ -139,13 +142,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", diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx index a08ff73a23..bb9e62addd 100644 --- a/webview-ui/src/components/settings/providers/ZooGateway.tsx +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -73,7 +73,7 @@ export const ZooGateway = ({ models={routerModels?.["zoo-gateway"] ?? {}} modelIdKey="zooGatewayModelId" serviceName="Zoo Gateway" - serviceUrl="https://www.zoocode.dev/dashboard" + serviceUrl="https://www.zoocode.dev/dashboard/models" organizationAllowList={organizationAllowList} errorMessage={modelValidationError} simplifySettings={simplifySettings} diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 71cf946a4f..316e3f66ac 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index b2b694db27..7fbb2021d6 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 8b75697591..c0591680bd 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -930,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", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 524d0b41c8..06f4fc3093 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 96ab1ac4de..1f98533162 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 12c6367959..b06fa07c94 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -867,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है। आप वर्तमान में उपलब्ध निःशुल्क विकल्पों के लिए \"free\" भी खोज सकते हैं।", + "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है।", "label": "मॉडल", "searchPlaceholder": "खोजें", "noMatchFound": "कोई मिलान नहीं मिला", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 4008b046bf..bc30c6e414 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4c2b1a5e98..39daaedb69 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index c5bfce6ea9..656f9846c4 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -867,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。また、「free」で検索すると、現在利用可能な無料オプションを見つけることができます。", + "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。", "label": "モデル", "searchPlaceholder": "検索", "noMatchFound": "一致するものが見つかりません", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index c0432d9165..3e054d28d0 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -867,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다. 현재 사용 가능한 무료 옵션을 찾으려면 \"free\"를 검색해 볼 수도 있습니다.", + "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다.", "label": "모델", "searchPlaceholder": "검색", "noMatchFound": "일치하는 항목 없음", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index b022717a94..0744ef9d32 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 460a8753b6..71fc221944 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -867,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ń", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index e02d741d5d..fd2f065cd4 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -867,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", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index f5473bb792..3772f8c749 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -867,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}. Также попробуйте поискать \"free\" для бесплатных вариантов.", + "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}.", "label": "Модель", "searchPlaceholder": "Поиск", "noMatchFound": "Совпадений не найдено", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 530ece1af9..e08b9bbe90 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -867,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ı", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 0384944ce1..c383443bbf 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -867,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ả", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 0d4908ef83..9f5e32226f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -867,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。您还可以搜索\"free\"以查找当前可用的免费选项。", + "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。", "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 1b42cfab41..309ed06bd1 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -877,7 +877,7 @@ } }, "modelPicker": { - "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。您也可以搜尋「free」來檢視目前可用的免費選項。", + "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。", "label": "模型", "searchPlaceholder": "搜尋", "noMatchFound": "找不到符合的結果",