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/README.md b/README.md index 3c954bcfee..bb632dff73 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ pre-release builds published automatically on every merge to `main`. - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) - ... - + --- diff --git a/locales/ca/README.md b/locales/ca/README.md index ceb134b3b9..3a4e8692b9 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -62,7 +62,7 @@ Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de mig - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/de/README.md b/locales/de/README.md index e2ecf8d32e..abd63539c8 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -62,7 +62,7 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Ro - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/es/README.md b/locales/es/README.md index e33f66563c..5805da5536 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -62,7 +62,7 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guí - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/fr/README.md b/locales/fr/README.md index 21d077b604..8530a55353 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -62,7 +62,7 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [gui - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/hi/README.md b/locales/hi/README.md index ed13dd3957..65b98a8df9 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -62,7 +62,7 @@ Roo Code से Zoo Code में आने के लिए एक quick guide - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/id/README.md b/locales/id/README.md index dbb9d21332..35ac4fc2c5 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -62,7 +62,7 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/it/README.md b/locales/it/README.md index d231c6b2f6..7d8887f27a 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -62,7 +62,7 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida al - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/ja/README.md b/locales/ja/README.md index feb5372a2a..f68cb51a22 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -62,7 +62,7 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/ko/README.md b/locales/ko/README.md index 2d98f04a76..3e26de7265 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -62,7 +62,7 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그 - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/nl/README.md b/locales/nl/README.md index c03dedd19b..7fc7218cc4 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -62,7 +62,7 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/pl/README.md b/locales/pl/README.md index 517ced2ba5..c9652650f7 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -62,7 +62,7 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 8649e88e9b..b92ba4c884 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -62,7 +62,7 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/ru/README.md b/locales/ru/README.md index c371b8b978..cb94ee5e15 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -62,7 +62,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/tr/README.md b/locales/tr/README.md index e99b0d543e..e61273a042 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -62,7 +62,7 @@ Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş reh - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/vi/README.md b/locales/vi/README.md index ef5ba75b8f..16ae18e18b 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -62,7 +62,7 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo C - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index bab13aa029..d0e1113a9a 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -62,7 +62,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 4fa055cc82..8c7810f211 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -62,7 +62,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... - + --- diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 1d92b7c08c..679987e610 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -38,6 +38,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 export const dynamicProviders = [ "openrouter", "vercel-ai-gateway", + "zoo-gateway", "litellm", "requesty", "roo", @@ -405,6 +406,12 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({ vercelAiGatewayModelId: z.string().optional(), }) +const zooGatewaySchema = baseProviderSettingsSchema.extend({ + zooSessionToken: z.string().optional(), + zooGatewayModelId: z.string().optional(), + zooGatewayBaseUrl: z.string().optional(), +}) + const basetenSchema = apiModelIdProviderModelSchema.extend({ basetenApiKey: z.string().optional(), }) @@ -444,6 +451,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, ]) @@ -479,6 +487,7 @@ export const providerSettingsSchema = z.object({ ...qwenCodeSchema.shape, ...rooSchema.shape, ...vercelAiGatewaySchema.shape, + ...zooGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, }) @@ -509,6 +518,7 @@ export const modelIdKeys = [ "unboundModelId", "litellmModelId", "vercelAiGatewayModelId", + "zooGatewayModelId", ] as const satisfies readonly (keyof ProviderSettings)[] export type ModelIdKey = (typeof modelIdKeys)[number] @@ -555,6 +565,7 @@ export const modelIdKeysByProvider: Record = { fireworks: "apiModelId", roo: "apiModelId", "vercel-ai-gateway": "vercelAiGatewayModelId", + "zoo-gateway": "zooGatewayModelId", } /** @@ -573,10 +584,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/") ) { @@ -677,6 +688,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 cd36a611f2..f8b54dec72 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -26,6 +26,7 @@ export * from "./vercel-ai-gateway.js" export * from "./zai.js" export * from "./minimax.js" export * from "./mimo.js" +export * from "./zoo-gateway.js" import { anthropicDefaultModelId } from "./anthropic.js" import { basetenDefaultModelId } from "./baseten.js" @@ -51,6 +52,7 @@ import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js" import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js" import { minimaxDefaultModelId } from "./minimax.js" import { mimoDefaultModelId } from "./mimo.js" +import { zooGatewayDefaultModelId } from "./zoo-gateway.js" // Import the ProviderName type from provider-settings to avoid duplication import type { ProviderName } from "../provider-settings.js" @@ -119,6 +121,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/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() }) 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/api/index.ts b/src/api/index.ts index c9e5e7b1b9..b50ac2492c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -32,6 +32,7 @@ import { ZAiHandler, FireworksHandler, VercelAiGatewayHandler, + ZooGatewayHandler, MiniMaxHandler, MimoHandler, BasetenHandler, @@ -176,6 +177,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new FireworksHandler(options) case "vercel-ai-gateway": return new VercelAiGatewayHandler(options) + case "zoo-gateway": + return new ZooGatewayHandler(options) case "minimax": return new MiniMaxHandler(options) case "baseten": diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 7e85fe7005..78e0cf6d80 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 => { 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 @@ -135,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/src/api/providers/fetchers/zoo-gateway.ts b/src/api/providers/fetchers/zoo-gateway.ts new file mode 100644 index 0000000000..82a8d3101b --- /dev/null +++ b/src/api/providers/fetchers/zoo-gateway.ts @@ -0,0 +1,111 @@ +import axios from "axios" + +import type { ModelInfo } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../../shared/api" +import { getZooCodeBaseUrl } from "../../../services/zoo-code-auth" + +// Reuse the same schemas and parsing logic from vercel-ai-gateway since the API format is identical +import { type VercelAiGatewayModel, parseVercelAiGatewayModel } from "./vercel-ai-gateway" + +import { z } from "zod" + +/** + * ZooGatewayPricing (same format as Vercel AI Gateway) + */ + +const zooGatewayPricingSchema = z.object({ + input: z.string().optional(), + output: z.string().optional(), + input_cache_write: z.string().optional(), + input_cache_read: z.string().optional(), + image: z.string().optional(), +}) + +/** + * ZooGatewayModel (same format as Vercel AI Gateway) + */ + +const zooGatewayModelSchema = z.object({ + id: z.string(), + object: z.string(), + created: z.number(), + owned_by: z.string(), + name: z.string(), + description: z.string(), + context_window: z.number(), + max_tokens: z.number(), + type: z.string(), + pricing: zooGatewayPricingSchema, +}) + +/** + * ZooGatewayModelsResponse + */ + +const zooGatewayModelsResponseSchema = z.object({ + object: z.string(), + data: z.array(zooGatewayModelSchema), +}) + +type ZooGatewayModelsResponse = z.infer + +/** + * getZooGatewayModels + * + * Fetches models from the Zoo Gateway API. Requires authentication via the zoo_ext_ token. + */ + +export async function getZooGatewayModels(options?: ApiHandlerOptions): Promise> { + const models: Record = {} + const baseURL = options?.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` + + // Build headers - Zoo Gateway requires authentication via the zoo_ext_ session token + const headers: Record = {} + if (options?.zooSessionToken) { + headers["Authorization"] = `Bearer ${options.zooSessionToken}` + } + + try { + const response = await axios.get(`${baseURL}/models`, { + headers, + }) + 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 9712e79fab..43a552a004 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -26,6 +26,7 @@ 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 { MimoHandler } from "./mimo" export { BasetenHandler } from "./baseten" diff --git a/src/api/providers/zoo-gateway.ts b/src/api/providers/zoo-gateway.ts new file mode 100644 index 0000000000..dc425ba508 --- /dev/null +++ b/src/api/providers/zoo-gateway.ts @@ -0,0 +1,179 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + zooGatewayDefaultModelId, + zooGatewayDefaultModelInfo, + ZOO_GATEWAY_DEFAULT_TEMPERATURE, + VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS, +} from "@roo-code/types" + +import { ApiHandlerOptions } from "../../shared/api" +import { getZooCodeBaseUrl } from "../../services/zoo-code-auth" + +import { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway" + +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { RouterProvider } from "./router-provider" + +import { DEFAULT_HEADERS } from "./constants" + +// Extend OpenAI's CompletionUsage to include Zoo Gateway specific fields (same as Vercel AI Gateway) +interface ZooGatewayUsage extends OpenAI.CompletionUsage { + cache_creation_input_tokens?: number + cost?: number +} + +export class ZooGatewayHandler extends RouterProvider implements SingleCompletionHandler { + constructor(options: ApiHandlerOptions) { + const baseURL = options.zooGatewayBaseUrl ?? `${getZooCodeBaseUrl()}/api/gateway/v1` + + // Fail fast with a clear message instead of waiting for a 401. + // The token is set automatically by handleZooCodeCallback() after the user + // authenticates via the "Sign in with Zoo Code" flow in the extension. + if (!options.zooSessionToken) { + throw new Error("Zoo Gateway requires authentication. Please sign in to Zoo Code first.") + } + + super({ + options, + name: "zoo-gateway", + baseURL, + apiKey: options.zooSessionToken, + modelId: options.zooGatewayModelId, + defaultModelId: zooGatewayDefaultModelId, + defaultModelInfo: zooGatewayDefaultModelInfo, + }) + + // Override the client to add Zoo-specific enrichment headers + // These headers help with request tracking and analytics + const enrichmentHeaders: Record = {} + + // Note: These headers will be populated per-request in createMessage + // For now we just set static headers that are always available + if (typeof process !== "undefined" && process.env?.npm_package_version) { + enrichmentHeaders["X-Zoo-Extension-Version"] = process.env.npm_package_version + } + enrichmentHeaders["X-Zoo-Editor"] = "vscode" + + // Recreate client with enrichment headers + ;(this as any).client = new OpenAI({ + baseURL, + apiKey: options.zooSessionToken, + defaultHeaders: { + ...DEFAULT_HEADERS, + ...enrichmentHeaders, + ...(options.openAiHeaders || {}), + }, + }) + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId, info } = await this.fetchModel() + + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + // Apply prompt caching for models that support it + // Zoo Gateway serves the same models as Vercel AI Gateway, so caching support is identical + if (VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS.has(modelId) && info.supportsPromptCache) { + addCacheBreakpoints(systemPrompt, openAiMessages) + } + + // Build request headers with enrichment metadata + const requestHeaders: Record = {} + if (metadata?.taskId) { + requestHeaders["X-Zoo-Task-ID"] = metadata.taskId + } + if (metadata?.mode) { + requestHeaders["X-Zoo-Mode"] = metadata.mode + } + + const body: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: openAiMessages, + temperature: this.supportsTemperature(modelId) + ? (this.options.modelTemperature ?? ZOO_GATEWAY_DEFAULT_TEMPERATURE) + : undefined, + max_completion_tokens: info.maxTokens, + stream: true, + stream_options: { include_usage: true }, + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? true, + } + + const completion = await this.client.chat.completions.create(body, { + headers: requestHeaders, + }) + + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + // Emit raw tool call chunks - NativeToolCallParser handles state management + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } + } + } + + if (chunk.usage) { + const usage = chunk.usage as ZooGatewayUsage + yield { + type: "usage", + inputTokens: usage.prompt_tokens || 0, + outputTokens: usage.completion_tokens || 0, + cacheWriteTokens: usage.cache_creation_input_tokens || undefined, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined, + totalCost: usage.cost ?? 0, + } + } + } + } + + async completePrompt(prompt: string): Promise { + const { id: modelId, info } = await this.fetchModel() + + try { + const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + stream: false, + } + + if (this.supportsTemperature(modelId)) { + requestOptions.temperature = this.options.modelTemperature ?? ZOO_GATEWAY_DEFAULT_TEMPERATURE + } + + requestOptions.max_completion_tokens = info.maxTokens + + const response = await this.client.chat.completions.create(requestOptions) + return response.choices[0]?.message.content || "" + } catch (error) { + if (error instanceof Error) { + throw new Error(`Zoo Gateway completion error: ${error.message}`) + } + throw error + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 37e99054b1..4983ae3a7d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -620,6 +620,10 @@ export class ClineProvider return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } + public static getAllInstances(): ClineProvider[] { + return Array.from(this.activeInstances) + } + public static async getInstance(): Promise { let visibleProvider = ClineProvider.getVisibleInstance() @@ -851,6 +855,64 @@ export class ClineProvider if (!currentTask || currentTask.abandoned || currentTask.abort) { await this.removeClineFromStack() } + + // Ensure zoo-gateway profile is seeded for users who signed in before this feature existed. + // Without this, users with a valid cached token but no zoo-gateway profile would need to + // re-authenticate to use Zoo Gateway. Fire-and-forget to avoid blocking webview init. + void this.ensureZooGatewayProfileSeeded().catch((err) => { + this.log(`[ensureZooGatewayProfileSeeded] Error: ${err instanceof Error ? err.message : String(err)}`) + }) + } + + /** + * Seeds the zoo-gateway provider profile for users who have a cached auth token + * but no profile (e.g., users who signed in before Zoo Gateway was added), or + * who have an empty/imported profile without a token. + * Called once per webview init; handleZooCodeCallback is idempotent so repeated calls are safe. + */ + private async ensureZooGatewayProfileSeeded(): Promise { + const { getCachedZooCodeToken } = await import("../../services/zoo-code-auth") + const token = getCachedZooCodeToken() + if (!token) return + + // Check ALL zoo-gateway profiles — only skip seeding if every profile has the current token. + // Using .find() would miss stale tokens in duplicate/renamed profiles since handleZooCodeCallback + // uses .filter() and updates all of them — the early-return guard must match. + const allProfiles = await this.providerSettingsManager.listConfig() + const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + + if (zooGatewayProfiles.length === 0) { + this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") + } else { + let allUpToDate = true + + for (const entry of zooGatewayProfiles) { + try { + const fullProfile = await this.providerSettingsManager.getProfile({ name: entry.name }) + if (fullProfile.zooSessionToken !== token) { + allUpToDate = false + this.log( + fullProfile.zooSessionToken + ? "[ensureZooGatewayProfileSeeded] Token mismatch (stale session?), updating with current token" + : "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token", + ) + break + } + } catch { + allUpToDate = false + this.log("[ensureZooGatewayProfileSeeded] Failed to read existing profile, will re-seed") + break + } + } + + if (allUpToDate) { + // All profiles have the current token — nothing to do + return + } + } + + // User has token but either no profile, some profiles without token, or stale tokens — seed all + await this.handleZooCodeCallback(token) } public async createTaskWithHistoryItem( @@ -1641,12 +1703,80 @@ export class ClineProvider await this.upsertProviderProfile(currentApiConfigName, newConfiguration) } - // Zoo Code Auth (for observability telemetry) + // Zoo Code Auth - async handleZooCodeCallback(_token: string) { + async handleZooCodeCallback(token: string) { // Auth mutation (token storage, subscription check, success toast) was already // performed by handleAuthCallback() in handleUri.ts before this method was called. - // This method only needs to refresh the webview state to reflect the new auth status. + // Save the zoo-gateway provider profile with the session token so that + // ZooGatewayHandler can authenticate without any manual user input. + // + // activate: true ONLY if Zoo Gateway is already the active profile — this pushes + // the new token to the in-memory handler so the current task picks it up immediately. + // Otherwise activate: false — do NOT switch providers mid-conversation. The user + // must explicitly select Zoo Gateway in settings if they want to use it. + try { + const { apiConfiguration } = await this.getState() + const currentSettings = this.contextProxy.getProviderSettings() + const currentApiConfigName = this.contextProxy.getValues().currentApiConfigName + + // Derive the gateway base URL from ZOO_CODE_BASE_URL so that non-prod environments + // (staging, local dev) route completions to the correct backend instead of always + // hard-coding production. An already-set value in the profile is NOT preserved here — + // it must always align with the auth server the user just authenticated against. + const { getZooCodeBaseUrl } = await import("../../services/zoo-code-auth") + const derivedGatewayBaseUrl = `${getZooCodeBaseUrl()}/api/gateway/v1` + + // Check if Zoo Gateway is the currently active profile by apiProvider identity, + // not by profile name (profile names are user-renameable). + const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + + // Always scan ALL profiles and update every zoo-gateway profile with the new token. + // This ensures renamed profiles, duplicate profiles, and inactive profiles all stay + // in sync. The model lookup in requestRouterModels uses .find() which returns the + // first zoo-gateway profile it finds — if that profile has a stale token, requests fail. + const allProfiles = await this.providerSettingsManager.listConfig() + const zooProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + + if (zooProfiles.length === 0) { + // No existing zoo-gateway profile — create the canonical default. + const newConfiguration: ProviderSettings = { + apiProvider: "zoo-gateway", + zooSessionToken: token, + zooGatewayModelId: apiConfiguration.zooGatewayModelId, + zooGatewayBaseUrl: derivedGatewayBaseUrl, + } + // Activate only if zoo-gateway was the active provider (shouldn't happen if + // no profiles exist, but defensive). + await this.upsertProviderProfile("Zoo Gateway", newConfiguration, isZooGatewayActive) + } else { + // Update every existing zoo-gateway profile with the new token and the + // derived base URL so that environment-specific routing stays consistent. + for (const entry of zooProfiles) { + const isActiveProfile = isZooGatewayActive && entry.name === currentApiConfigName + const existing = await this.providerSettingsManager.getProfile({ name: entry.name }) + const updated: ProviderSettings = { + ...existing, + zooSessionToken: token, + zooGatewayBaseUrl: derivedGatewayBaseUrl, + } + if (isActiveProfile) { + // Use upsertProviderProfile with activate: true so the in-memory handler + // picks up the new token immediately for the current task. + await this.upsertProviderProfile(entry.name, updated, true) + } else { + // Non-active profiles just need the token saved to disk. + await this.providerSettingsManager.saveConfig(entry.name, updated) + } + } + } + } catch (error) { + this.log( + `[handleZooCodeCallback] Failed to save zoo-gateway profile: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + } await this.postStateToWebview() } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index d25c6971d2..18c18b89e0 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2473,11 +2473,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: {}, }, @@ -2509,6 +2510,7 @@ describe("ClineProvider - Router Models", () => { .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail .mockResolvedValueOnce(mockModels) // unbound success .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success + .mockResolvedValueOnce(mockModels) // zoo-gateway success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail await messageHandler({ type: "requestRouterModels" }) @@ -2520,11 +2522,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: {}, }, @@ -2616,11 +2619,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, { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index dc029cb7dd..dd46fdd76f 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: {}, @@ -945,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" } }, @@ -964,6 +987,14 @@ export const webviewMessageHandler = async ( }, }, { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, + { + key: "zoo-gateway", + options: { + provider: "zoo-gateway", + apiKey: zooGatewayToken, + baseUrl: zooGatewayBaseUrl, + }, + }, ] // LiteLLM is conditional on baseUrl+apiKey @@ -2432,6 +2463,51 @@ export const webviewMessageHandler = async ( try { const { disconnectZooCode } = await import("../../services/zoo-code-auth") await disconnectZooCode() + + // Clear zooSessionToken from ALL provider profiles with apiProvider === "zoo-gateway". + // Profiles are user-renameable, so we cannot rely on a hardcoded name like "Zoo Gateway". + // We must scan all profiles and clear tokens from any that use the zoo-gateway provider. + try { + const allProfiles = await provider.providerSettingsManager.listConfig() + // Check if Zoo Gateway is the currently active profile by apiProvider identity + const currentSettings = provider.contextProxy.getProviderSettings() + const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + const currentApiConfigName = provider.contextProxy.getValues().currentApiConfigName + + for (const entry of allProfiles) { + if (entry.apiProvider === "zoo-gateway") { + const profile = await provider.providerSettingsManager.getProfile({ name: entry.name }) + 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`, + ) + } + } + } + } + } 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( 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 = { diff --git a/src/shared/api.ts b/src/shared/api.ts index 0970efb4b4..fa6c8b5789 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -171,6 +171,7 @@ type CommonFetchParams = { const dynamicProviderExtras = { openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type "vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + "zoo-gateway": {} as { apiKey?: string; baseUrl?: string }, litellm: {} as { apiKey: string; baseUrl: string }, requesty: {} as { apiKey?: string; baseUrl?: string }, unbound: {} as { apiKey?: string }, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index bc254a189d..e63f62744a 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -93,6 +93,7 @@ import { ZAi, Fireworks, VercelAiGateway, + ZooGateway, MiniMax, Mimo, } from "./providers" @@ -133,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 || {} @@ -272,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) => { @@ -697,6 +706,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 { zooCodeIsAuthenticated, zooCodeUserEmail, zooCodeUserName, zooCodeBaseUrl, uriScheme, deviceName } = + useExtensionState() + + const authUrl = getZooCodeAuthUrl(uriScheme, zooCodeBaseUrl, deviceName) + + return ( + <> + {/* Zoo Code Authentication Section */} +
+
+ + {zooCodeIsAuthenticated && zooCodeUserEmail && ( + {zooCodeUserEmail} + )} +
+ {!zooCodeIsAuthenticated ? ( +
+

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

+ + {t("settings:providers.zooGateway.signInButton")} + +
+ ) : ( +
+ + + {zooCodeUserName + ? t("settings:providers.zooGateway.authenticatedAs", { name: zooCodeUserName }) + : t("settings:providers.zooGateway.authenticated")} + +
+ )} +
+ + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 02a928ffb5..6811e13154 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -22,6 +22,7 @@ export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" export { VercelAiGateway } from "./VercelAiGateway" +export { ZooGateway } from "./ZooGateway" export { MiniMax } from "./MiniMax" export { Mimo } from "./Mimo" export { Baseten } from "./Baseten" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 02585889c7..cb9eda0cd3 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -351,6 +351,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/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx index ef571b08cc..fcaac6fbd8 100644 --- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx +++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx @@ -16,7 +16,8 @@ import { Trans } from "react-i18next" import { ArrowLeft, Brain } from "lucide-react" const WelcomeViewProvider = () => { - const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState() + const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, zooCodeIsAuthenticated } = + useExtensionState() const { t } = useAppTranslation() const [errorMessage, setErrorMessage] = useState(undefined) const [showProviderSetup, setShowProviderSetup] = useState(false) @@ -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 92ecfa7e73..316e3f66ac 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "El proveïdor '{{provider}}' no està permès per la vostra organització", "modelNotAllowed": "El model '{{model}}' no està permès per al proveïdor '{{provider}}' per la vostra organització", "profileInvalid": "Aquest perfil conté un proveïdor o model que no està permès per la vostra organització", - "qwenCodeOauthPath": "Has de proporcionar una ruta vàlida de credencials OAuth" + "qwenCodeOauthPath": "Has de proporcionar una ruta vàlida de credencials OAuth", + "zooGatewaySignIn": "Has d'iniciar sessió a Zoo Code per utilitzar Zoo Gateway. Fes clic a 'Inicia sessió' per autenticar-te." }, "placeholders": { "apiKey": "Introduïu la clau API...", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 5154d11039..7fbb2021d6 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "Anbieter '{{provider}}' ist von deiner Organisation nicht erlaubt", "modelNotAllowed": "Modell '{{model}}' ist für Anbieter '{{provider}}' von deiner Organisation nicht erlaubt", "profileInvalid": "Dieses Profil enthält einen Anbieter oder ein Modell, das von deiner Organisation nicht erlaubt ist", - "qwenCodeOauthPath": "Du musst einen gültigen OAuth-Anmeldedaten-Pfad angeben" + "qwenCodeOauthPath": "Du musst einen gültigen OAuth-Anmeldedaten-Pfad angeben", + "zooGatewaySignIn": "Du musst dich bei Zoo Code anmelden, um Zoo Gateway zu verwenden. Klicke auf 'Anmelden', um dich zu authentifizieren." }, "placeholders": { "apiKey": "API-Schlüssel eingeben...", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index a3c11be386..c0591680bd 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": { @@ -923,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", @@ -961,7 +968,8 @@ "providerNotAllowed": "Provider '{{provider}}' is not allowed by your organization", "modelNotAllowed": "Model '{{model}}' is not allowed for provider '{{provider}}' by your organization", "profileInvalid": "This profile contains a provider or model that is not allowed by your organization", - "qwenCodeOauthPath": "You must provide a valid OAuth credentials path." + "qwenCodeOauthPath": "You must provide a valid OAuth credentials path.", + "zooGatewaySignIn": "You must sign in to Zoo Code to use Zoo Gateway. Click 'Sign In' to authenticate." }, "placeholders": { "apiKey": "Enter API Key...", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 8ec3e29338..06f4fc3093 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "El proveedor '{{provider}}' no está permitido por su organización", "modelNotAllowed": "El modelo '{{model}}' no está permitido para el proveedor '{{provider}}' por su organización", "profileInvalid": "Este perfil contiene un proveedor o modelo que no está permitido por su organización", - "qwenCodeOauthPath": "Debes proporcionar una ruta válida de credenciales OAuth" + "qwenCodeOauthPath": "Debes proporcionar una ruta válida de credenciales OAuth", + "zooGatewaySignIn": "Debes iniciar sesión en Zoo Code para usar Zoo Gateway. Haz clic en 'Iniciar sesión' para autenticarte." }, "placeholders": { "apiKey": "Ingrese clave API...", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index d746111a0a..1f98533162 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "Le fournisseur '{{provider}}' n'est pas autorisé par votre organisation", "modelNotAllowed": "Le modèle '{{model}}' n'est pas autorisé pour le fournisseur '{{provider}}' par votre organisation", "profileInvalid": "Ce profil contient un fournisseur ou un modèle qui n'est pas autorisé par votre organisation", - "qwenCodeOauthPath": "Tu dois fournir un chemin valide pour les identifiants OAuth" + "qwenCodeOauthPath": "Tu dois fournir un chemin valide pour les identifiants OAuth", + "zooGatewaySignIn": "Tu dois te connecter à Zoo Code pour utiliser Zoo Gateway. Clique sur 'Se connecter' pour t'authentifier." }, "placeholders": { "apiKey": "Saisissez la clé API...", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 9a77b69ee4..b06fa07c94 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": { @@ -860,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है। आप वर्तमान में उपलब्ध निःशुल्क विकल्पों के लिए \"free\" भी खोज सकते हैं।", + "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है।", "label": "मॉडल", "searchPlaceholder": "खोजें", "noMatchFound": "कोई मिलान नहीं मिला", @@ -898,7 +905,8 @@ "providerNotAllowed": "प्रदाता '{{provider}}' आपके संगठन द्वारा अनुमत नहीं है", "modelNotAllowed": "मॉडल '{{model}}' प्रदाता '{{provider}}' के लिए आपके संगठन द्वारा अनुमत नहीं है", "profileInvalid": "इस प्रोफ़ाइल में एक प्रदाता या मॉडल शामिल है जो आपके संगठन द्वारा अनुमत नहीं है", - "qwenCodeOauthPath": "आपको एक वैध OAuth क्रेडेंशियल पथ प्रदान करना होगा" + "qwenCodeOauthPath": "आपको एक वैध OAuth क्रेडेंशियल पथ प्रदान करना होगा", + "zooGatewaySignIn": "Zoo Gateway का उपयोग करने के लिए आपको Zoo Code में साइन इन करना होगा। प्रमाणीकृत करने के लिए 'साइन इन' पर क्लिक करें।" }, "placeholders": { "apiKey": "API कुंजी दर्ज करें...", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 41eae1b053..bc30c6e414 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "Provider '{{provider}}' tidak diizinkan oleh organisasi kamu", "modelNotAllowed": "Model '{{model}}' tidak diizinkan untuk provider '{{provider}}' oleh organisasi kamu", "profileInvalid": "Profil ini berisi provider atau model yang tidak diizinkan oleh organisasi kamu", - "qwenCodeOauthPath": "Kamu harus memberikan jalur kredensial OAuth yang valid" + "qwenCodeOauthPath": "Kamu harus memberikan jalur kredensial OAuth yang valid", + "zooGatewaySignIn": "Kamu harus masuk ke Zoo Code untuk menggunakan Zoo Gateway. Klik 'Masuk' untuk mengautentikasi." }, "placeholders": { "apiKey": "Masukkan API Key...", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 806fb44462..39daaedb69 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "Il fornitore '{{provider}}' non è consentito dalla tua organizzazione", "modelNotAllowed": "Il modello '{{model}}' non è consentito per il fornitore '{{provider}}' dalla tua organizzazione.", "profileInvalid": "Questo profilo contiene un fornitore o un modello non consentito dalla tua organizzazione.", - "qwenCodeOauthPath": "Devi fornire un percorso valido per le credenziali OAuth" + "qwenCodeOauthPath": "Devi fornire un percorso valido per le credenziali OAuth", + "zooGatewaySignIn": "Devi accedere a Zoo Code per utilizzare Zoo Gateway. Clicca su 'Accedi' per autenticarti." }, "placeholders": { "apiKey": "Inserisci chiave API...", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b9a0976c42..656f9846c4 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": { @@ -860,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。また、「free」で検索すると、現在利用可能な無料オプションを見つけることができます。", + "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。", "label": "モデル", "searchPlaceholder": "検索", "noMatchFound": "一致するものが見つかりません", @@ -898,7 +905,8 @@ "providerNotAllowed": "プロバイダー「{{provider}}」は組織によって許可されていません", "modelNotAllowed": "モデル「{{model}}」はプロバイダー「{{provider}}」に対して組織によって許可されていません", "profileInvalid": "このプロファイルには、組織によって許可されていないプロバイダーまたはモデルが含まれています", - "qwenCodeOauthPath": "有効なOAuth認証情報のパスを提供する必要があります" + "qwenCodeOauthPath": "有効なOAuth認証情報のパスを提供する必要があります", + "zooGatewaySignIn": "Zoo Gatewayを使用するにはZoo Codeにサインインする必要があります。認証するには「サインイン」をクリックしてください。" }, "placeholders": { "apiKey": "API キーを入力...", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 3a88c9fbde..3e054d28d0 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": { @@ -860,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다. 현재 사용 가능한 무료 옵션을 찾으려면 \"free\"를 검색해 볼 수도 있습니다.", + "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다.", "label": "모델", "searchPlaceholder": "검색", "noMatchFound": "일치하는 항목 없음", @@ -898,7 +905,8 @@ "providerNotAllowed": "제공자 '{{provider}}'는 조직에서 허용되지 않습니다", "modelNotAllowed": "모델 '{{model}}'은 제공자 '{{provider}}'에 대해 조직에서 허용되지 않습니다", "profileInvalid": "이 프로필에는 조직에서 허용되지 않는 제공자 또는 모델이 포함되어 있습니다", - "qwenCodeOauthPath": "유효한 OAuth 자격 증명 경로를 제공해야 합니다" + "qwenCodeOauthPath": "유효한 OAuth 자격 증명 경로를 제공해야 합니다", + "zooGatewaySignIn": "Zoo Gateway를 사용하려면 Zoo Code에 로그인해야 합니다. 인증하려면 '로그인'을 클릭하세요." }, "placeholders": { "apiKey": "API 키 입력...", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index a91bb87de5..0744ef9d32 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "Provider '{{provider}}' is niet toegestaan door je organisatie", "modelNotAllowed": "Model '{{model}}' is niet toegestaan voor provider '{{provider}}' door je organisatie", "profileInvalid": "Dit profiel bevat een provider of model dat niet is toegestaan door je organisatie", - "qwenCodeOauthPath": "Je moet een geldig OAuth-referentiepad opgeven" + "qwenCodeOauthPath": "Je moet een geldig OAuth-referentiepad opgeven", + "zooGatewaySignIn": "Je moet inloggen bij Zoo Code om Zoo Gateway te gebruiken. Klik op 'Inloggen' om je te authenticeren." }, "placeholders": { "apiKey": "Voer API-sleutel in...", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index f3751196d8..71fc221944 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": { @@ -860,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ń", @@ -898,7 +905,8 @@ "providerNotAllowed": "Dostawca '{{provider}}' nie jest dozwolony przez Twoją organizację", "modelNotAllowed": "Model '{{model}}' nie jest dozwolony dla dostawcy '{{provider}}' przez Twoją organizację", "profileInvalid": "Ten profil zawiera dostawcę lub model, który nie jest dozwolony przez Twoją organizację", - "qwenCodeOauthPath": "Musisz podać prawidłową ścieżkę do poświadczeń OAuth" + "qwenCodeOauthPath": "Musisz podać prawidłową ścieżkę do poświadczeń OAuth", + "zooGatewaySignIn": "Musisz zalogować się do Zoo Code, aby korzystać z Zoo Gateway. Kliknij 'Zaloguj się', aby się uwierzytelnić." }, "placeholders": { "apiKey": "Wprowadź klucz API...", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 05d3557f76..fd2f065cd4 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": { @@ -860,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", @@ -898,7 +905,8 @@ "providerNotAllowed": "O provedor '{{provider}}' não é permitido pela sua organização", "modelNotAllowed": "O modelo '{{model}}' não é permitido para o provedor '{{provider}}' pela sua organização", "profileInvalid": "Este perfil contém um provedor ou modelo que não é permitido pela sua organização", - "qwenCodeOauthPath": "Você deve fornecer um caminho válido de credenciais OAuth" + "qwenCodeOauthPath": "Você deve fornecer um caminho válido de credenciais OAuth", + "zooGatewaySignIn": "Você deve fazer login no Zoo Code para usar o Zoo Gateway. Clique em 'Entrar' para autenticar." }, "placeholders": { "apiKey": "Digite a chave API...", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 1aedb8a575..3772f8c749 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": { @@ -860,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}. Также попробуйте поискать \"free\" для бесплатных вариантов.", + "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}.", "label": "Модель", "searchPlaceholder": "Поиск", "noMatchFound": "Совпадений не найдено", @@ -898,7 +905,8 @@ "providerNotAllowed": "Провайдер '{{provider}}' не разрешен вашей организацией", "modelNotAllowed": "Модель '{{model}}' не разрешена для провайдера '{{provider}}' вашей организацией", "profileInvalid": "Этот профиль содержит провайдера или модель, которые не разрешены вашей организацией", - "qwenCodeOauthPath": "Вы должны указать допустимый путь к учетным данным OAuth" + "qwenCodeOauthPath": "Вы должны указать допустимый путь к учетным данным OAuth", + "zooGatewaySignIn": "Для использования Zoo Gateway необходимо войти в Zoo Code. Нажмите «Войти», чтобы пройти аутентификацию." }, "placeholders": { "apiKey": "Введите API-ключ...", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 22bd730488..e08b9bbe90 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": { @@ -860,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ı", @@ -898,7 +905,8 @@ "providerNotAllowed": "Sağlayıcı '{{provider}}' kuruluşunuz tarafından izin verilmiyor", "modelNotAllowed": "Model '{{model}}' sağlayıcı '{{provider}}' için kuruluşunuz tarafından izin verilmiyor", "profileInvalid": "Bu profil, kuruluşunuz tarafından izin verilmeyen bir sağlayıcı veya model içeriyor", - "qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısın" + "qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısın", + "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 0b09ae004b..c383443bbf 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": { @@ -860,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ả", @@ -898,7 +905,8 @@ "providerNotAllowed": "Nhà cung cấp '{{provider}}' không được phép bởi tổ chức của bạn", "modelNotAllowed": "Mô hình '{{model}}' không được phép cho nhà cung cấp '{{provider}}' bởi tổ chức của bạn", "profileInvalid": "Hồ sơ này chứa một nhà cung cấp hoặc mô hình không được phép bởi tổ chức của bạn", - "qwenCodeOauthPath": "Bạn phải cung cấp đường dẫn thông tin xác thực OAuth hợp lệ" + "qwenCodeOauthPath": "Bạn phải cung cấp đường dẫn thông tin xác thực OAuth hợp lệ", + "zooGatewaySignIn": "Bạn phải đăng nhập vào Zoo Code để sử dụng Zoo Gateway. Nhấp vào 'Đăng nhập' để xác thực." }, "placeholders": { "apiKey": "Nhập khóa API...", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index f58ea87f8c..9f5e32226f 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": { @@ -860,7 +867,7 @@ } }, "modelPicker": { - "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。您还可以搜索\"free\"以查找当前可用的免费选项。", + "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。", "label": "模型", "searchPlaceholder": "搜索", "noMatchFound": "未找到匹配项", @@ -898,7 +905,8 @@ "providerNotAllowed": "提供商 '{{provider}}' 不允许用于您的组织", "modelNotAllowed": "模型 '{{model}}' 不允许用于提供商 '{{provider}}',您的组织不允许", "profileInvalid": "此配置文件包含您的组织不允许的提供商或模型", - "qwenCodeOauthPath": "您必须提供有效的 OAuth 凭证路径" + "qwenCodeOauthPath": "您必须提供有效的 OAuth 凭证路径", + "zooGatewaySignIn": "您必须登录 Zoo Code 才能使用 Zoo Gateway。点击「登录」进行身份验证。" }, "placeholders": { "apiKey": "请输入 API 密钥...", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index e4268280de..309ed06bd1 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": { @@ -870,7 +877,7 @@ } }, "modelPicker": { - "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。您也可以搜尋「free」來檢視目前可用的免費選項。", + "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。", "label": "模型", "searchPlaceholder": "搜尋", "noMatchFound": "找不到符合的結果", @@ -908,7 +915,8 @@ "providerNotAllowed": "供應商 '{{provider}}' 不允許用於您的組織。", "modelNotAllowed": "模型 '{{model}}' 不允許用於供應商 '{{provider}}',此設定已被組織禁止", "profileInvalid": "此設定檔包含您的組織不允許的供應商或模型", - "qwenCodeOauthPath": "您必須提供有效的 OAuth 憑證路徑" + "qwenCodeOauthPath": "您必須提供有效的 OAuth 憑證路徑", + "zooGatewaySignIn": "您必須登入 Zoo Code 才能使用 Zoo Gateway。點擊「登入」進行身份驗證。" }, "placeholders": { "apiKey": "請輸入 API 金鑰...", diff --git a/webview-ui/src/utils/__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: {}, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index f506171acc..30c8af2d01 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -17,8 +17,9 @@ export function validateApiConfiguration( apiConfiguration: ProviderSettings, routerModels?: RouterModels, organizationAllowList?: OrganizationAllowList, + zooCodeIsAuthenticated?: boolean, ): string | undefined { - const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated) if (keysAndIdsPresentErrorMessage) { return keysAndIdsPresentErrorMessage @@ -36,7 +37,10 @@ export function validateApiConfiguration( return validateDynamicProviderModelId(apiConfiguration, routerModels) } -function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): string | undefined { +function validateModelsAndKeysProvided( + apiConfiguration: ProviderSettings, + zooCodeIsAuthenticated?: boolean, +): string | undefined { switch (apiConfiguration.apiProvider) { case "openrouter": if (!apiConfiguration.openRouterApiKey) { @@ -123,6 +127,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "zoo-gateway": + if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { + return i18next.t("settings:validation.zooGatewaySignIn") + } + break case "baseten": if (!apiConfiguration.basetenApiKey) { return i18next.t("settings:validation.apiKey") @@ -282,8 +291,9 @@ export function validateApiConfigurationExcludingModelErrors( apiConfiguration: ProviderSettings, _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, + zooCodeIsAuthenticated?: boolean, ): string | undefined { - const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration, zooCodeIsAuthenticated) if (keysAndIdsPresentErrorMessage) { return keysAndIdsPresentErrorMessage