diff --git a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts index 3be70864ea..b3668a681d 100644 --- a/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/zoo-gateway.spec.ts @@ -5,6 +5,10 @@ import axios from "axios" import { getZooGatewayModels, parseZooGatewayModel } from "../zoo-gateway" vitest.mock("axios") +vitest.mock("../../../../services/zoo-code-auth", () => ({ + getCachedZooCodeToken: vitest.fn(() => ""), + getZooCodeBaseUrl: vitest.fn(() => "https://example.test"), +})) const mockedAxios = axios as any describe("Zoo Gateway Fetchers", () => { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 2b724e6dde..60db179553 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -31,6 +31,7 @@ import { fireworksDefaultModelId, vercelAiGatewayDefaultModelId, opencodeGoDefaultModelId, + zooGatewayDefaultModelId, minimaxDefaultModelId, mimoDefaultModelId, unboundDefaultModelId, @@ -95,6 +96,7 @@ import { Fireworks, VercelAiGateway, OpenCodeGo, + ZooGateway, MiniMax, Mimo, } from "./providers" @@ -268,6 +270,14 @@ const ApiOptions = ({ return } + // Zoo Gateway renders its own auth-state error inline (sign-in card in + // ZooGateway.tsx) so it can react to zooCodeIsAuthenticated changes + // without re-running this effect or threading auth state through validation. + if (apiConfiguration.apiProvider === "zoo-gateway") { + setErrorMessage(undefined) + return + } + const apiValidationResult = validateApiConfigurationExcludingModelErrors( apiConfiguration, routerModels, @@ -366,6 +376,7 @@ const ApiOptions = ({ poe: { field: "apiModelId", default: poeDefaultModelId }, "vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId }, "opencode-go": { field: "opencodeGoModelId", default: opencodeGoDefaultModelId }, + "zoo-gateway": { field: "zooGatewayModelId", default: zooGatewayDefaultModelId }, openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, lmstudio: { field: "lmStudioModelId" }, @@ -702,6 +713,17 @@ const ApiOptions = ({ /> )} + {selectedProvider === "zoo-gateway" && ( + + )} + {selectedProvider === "fireworks" && ( { expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiModelId", openAiCodexDefaultModelId, false) }) + it("initializes zooGatewayModelId to its default when switching provider to zoo-gateway", () => { + // Regression: zoo-gateway was previously missing from PROVIDER_MODEL_CONFIG, so switching + // providers never seeded zooGatewayModelId. Configs were left without a model id, which + // blocked completion flows that require a dynamic-provider model id. + const mockSetApiConfigurationField = vi.fn() + + renderApiOptions({ + apiConfiguration: { + apiProvider: "anthropic", + // No prior zooGatewayModelId. + }, + setApiConfigurationField: mockSetApiConfigurationField, + }) + + const providerSelectContainer = screen.getByTestId("provider-select") + const providerSelect = providerSelectContainer.querySelector("select") as HTMLSelectElement + fireEvent.change(providerSelect, { target: { value: "zoo-gateway" } }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiProvider", "zoo-gateway") + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("zooGatewayModelId", zooGatewayDefaultModelId, false) + }) + it("shows temperature and rate limit controls by default", () => { renderApiOptions({ apiConfiguration: {}, diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 6769bda65d..566370c837 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -64,6 +64,7 @@ export const PROVIDERS = [ { value: "fireworks", label: "Fireworks AI", proxy: false }, { value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false }, { value: "opencode-go", label: "Opencode Go", proxy: false }, + { value: "zoo-gateway", label: "Zoo Gateway", proxy: false }, { value: "minimax", label: "MiniMax", proxy: false }, { value: "mimo", label: "Xiaomi MiMo", proxy: false }, { value: "baseten", label: "Baseten", proxy: false }, diff --git a/webview-ui/src/components/settings/providers/ZooGateway.tsx b/webview-ui/src/components/settings/providers/ZooGateway.tsx new file mode 100644 index 0000000000..ac99f464a4 --- /dev/null +++ b/webview-ui/src/components/settings/providers/ZooGateway.tsx @@ -0,0 +1,125 @@ +import { useEffect, useMemo } from "react" +import { + type ProviderSettings, + type OrganizationAllowList, + type RouterModels, + zooGatewayDefaultModelId, +} from "@roo-code/types" + +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { getZooCodeAuthUrl } from "@src/oauth/urls" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { ModelPicker } from "../ModelPicker" +import { ApiErrorMessage } from "../ApiErrorMessage" + +type ZooGatewayProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +function isClaudeSonnetModelId(id: string) { + return /claude.*sonnet/i.test(id) +} + +// Exported for unit tests. Picks the default Zoo Gateway model id, preferring +// Claude Sonnet 4.5 → Sonnet 4 → first available Sonnet → first model overall. +export function pickZooGatewayDefaultModelId(modelIds: string[]) { + if (modelIds.length === 0) { + return zooGatewayDefaultModelId + } + + const sonnets = modelIds.filter(isClaudeSonnetModelId) + if (sonnets.length === 0) { + return modelIds[0] + } + + return ( + sonnets.find((id) => id === "anthropic/claude-sonnet-4.5") ?? + sonnets.find((id) => id.includes("claude-sonnet-4.5")) ?? + sonnets.find((id) => /sonnet-4[.-]5/i.test(id)) ?? + sonnets.find((id) => /sonnet-4(?![.-]?\d)/i.test(id)) ?? + sonnets[0] + ) +} + +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) + const resolvedDashboardBase = zooCodeBaseUrl?.replace(/\/$/, "") || "https://www.zoocode.dev" + + const zooModels = useMemo(() => routerModels?.["zoo-gateway"] ?? {}, [routerModels]) + const modelIds = useMemo(() => Object.keys(zooModels), [zooModels]) + const resolvedDefaultModelId = useMemo(() => pickZooGatewayDefaultModelId(modelIds), [modelIds]) + + useEffect(() => { + if (modelIds.length === 0) { + return + } + + const current = apiConfiguration.zooGatewayModelId + if (!current || !modelIds.includes(current)) { + setApiConfigurationField("zooGatewayModelId", resolvedDefaultModelId) + } + }, [apiConfiguration.zooGatewayModelId, modelIds, resolvedDefaultModelId, setApiConfigurationField]) + + return ( + <> +
+
+ + {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/__tests__/ZooGateway.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx new file mode 100644 index 0000000000..9bdda1f433 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/ZooGateway.spec.tsx @@ -0,0 +1,189 @@ +import React from "react" +import { render, screen, waitFor } from "@/utils/test-utils" +import type { ModelInfo, ProviderSettings, RouterModels } from "@roo-code/types" + +import { ZooGateway, pickZooGatewayDefaultModelId } from "../ZooGateway" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +const extensionStateMock = { + zooCodeIsAuthenticated: true, + zooCodeUserEmail: "user@example.com", + zooCodeUserName: "User", + zooCodeBaseUrl: "https://www.zoocode.dev", + uriScheme: "vscode", + deviceName: "Test Device", +} + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => extensionStateMock, +})) + +vi.mock("@src/oauth/urls", () => ({ + getZooCodeAuthUrl: () => "https://www.zoocode.dev/dashboard/connect", +})) + +vi.mock("../../ModelPicker", () => ({ + ModelPicker: ({ defaultModelId }: { defaultModelId: string }) => ( +
+ ), +})) + +const baseInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1, + outputPrice: 2, +} + +function buildRouterModels(modelIds: string[]): RouterModels { + const models = Object.fromEntries(modelIds.map((id) => [id, baseInfo])) + return { "zoo-gateway": models } as unknown as RouterModels +} + +describe("pickZooGatewayDefaultModelId", () => { + it("falls back to the static default when the catalog is empty", () => { + expect(pickZooGatewayDefaultModelId([])).toBe("anthropic/claude-sonnet-4") + }) + + it("prefers an exact anthropic/claude-sonnet-4.5 match", () => { + const result = pickZooGatewayDefaultModelId([ + "anthropic/claude-sonnet-4", + "anthropic/claude-sonnet-4.5", + "openai/gpt-4o", + ]) + expect(result).toBe("anthropic/claude-sonnet-4.5") + }) + + it("matches a Bedrock-style claude-sonnet-4-5 id", () => { + const result = pickZooGatewayDefaultModelId([ + "anthropic.claude-sonnet-4-20250514-v1:0", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + ]) + expect(result).toBe("anthropic.claude-sonnet-4-5-20250929-v1:0") + }) + + it("falls back to claude sonnet 4 when 4.5 is not in the catalog", () => { + const result = pickZooGatewayDefaultModelId(["openai/gpt-4o", "anthropic/claude-sonnet-4"]) + expect(result).toBe("anthropic/claude-sonnet-4") + }) + + it("falls back to the first available id when no claude sonnet is present", () => { + const result = pickZooGatewayDefaultModelId(["openai/gpt-4o", "google/gemini-2.5-pro"]) + expect(result).toBe("openai/gpt-4o") + }) +}) + +describe("ZooGateway component", () => { + const baseProps = { + organizationAllowList: { allowAll: true, providers: {} } as ProviderSettings extends never ? never : any, + setApiConfigurationField: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("auto-selects the resolved default model when the profile has no model id", async () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + await waitFor(() => { + expect(setApiConfigurationField).toHaveBeenCalledWith("zooGatewayModelId", "anthropic/claude-sonnet-4.5") + }) + }) + + it("reassigns a stale model id that is not in the catalog", async () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + await waitFor(() => { + expect(setApiConfigurationField).toHaveBeenCalledWith( + "zooGatewayModelId", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + ) + }) + }) + + it("does not overwrite a model id that is already valid for the catalog", async () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + await waitFor(() => { + expect(setApiConfigurationField).not.toHaveBeenCalled() + }) + }) + + it("does nothing while the catalog is still empty (router models loading)", () => { + const setApiConfigurationField = vi.fn() + render( + , + ) + + expect(setApiConfigurationField).not.toHaveBeenCalled() + }) + + it("renders the sign-in validation error inline when not authenticated", () => { + const original = extensionStateMock.zooCodeIsAuthenticated + extensionStateMock.zooCodeIsAuthenticated = false + try { + render( + , + ) + + expect(screen.getByText("settings:validation.zooGatewaySignIn")).toBeInTheDocument() + } finally { + extensionStateMock.zooCodeIsAuthenticated = original + } + }) +}) diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 1e10979c63..d5dd0d0ded 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -23,6 +23,7 @@ export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" export { VercelAiGateway } from "./VercelAiGateway" export { OpenCodeGo } from "./OpenCodeGo" +export { ZooGateway } from "./ZooGateway" export { MiniMax } from "./MiniMax" export { Mimo } from "./Mimo" export { Baseten } from "./Baseten" diff --git a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx index 7667d32ad6..fdde35320f 100644 --- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx +++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx @@ -36,7 +36,8 @@ const getWelcomeApiConfiguration = (apiConfiguration?: ProviderSettings): Provid } const WelcomeViewProvider = () => { - const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState() + const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, zooCodeIsAuthenticated } = + useExtensionState() const { t } = useAppTranslation() const [errorMessage, setErrorMessage] = useState(undefined) const [showProviderSetup, setShowProviderSetup] = useState(false) @@ -65,7 +66,7 @@ const WelcomeViewProvider = () => { return } - const error = validateApiConfiguration(effectiveApiConfiguration) + const error = validateApiConfiguration(effectiveApiConfiguration, undefined, undefined, zooCodeIsAuthenticated) if (error) { setErrorMessage(error) @@ -78,7 +79,14 @@ const WelcomeViewProvider = () => { text: currentApiConfigName, apiConfiguration: effectiveApiConfiguration, }) - }, [showProviderSetup, apiConfiguration, setApiConfiguration, effectiveApiConfiguration, currentApiConfigName]) + }, [ + showProviderSetup, + apiConfiguration, + setApiConfiguration, + effectiveApiConfiguration, + currentApiConfigName, + zooCodeIsAuthenticated, + ]) if (!showProviderSetup) { return ( diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index f7e3bb69e2..b9dacee76d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 1aa9f96053..00e68a6f60 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 7f3527df6b..aaeb6ad7c9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -553,6 +553,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": { @@ -969,7 +976,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 154319baef..461063d562 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 30cdd37785..89def36ad1 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 a334b8cb5f..426fcefb72 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -560,6 +560,13 @@ "placeholder": "डिफ़ॉल्ट: claude", "maxTokensLabel": "अधिकतम आउटपुट टोकन", "maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।" + }, + "zooGateway": { + "account": "Zoo Code खाता", + "signInButton": "Zoo Code में साइन इन करें", + "signInDescription": "अपने खाते के साथ Zoo Gateway का उपयोग करने के लिए साइन इन करें", + "authenticated": "प्रमाणित", + "authenticatedAs": "{{name}} के रूप में प्रमाणित" } }, "checkpoints": { @@ -901,7 +908,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 23e974429e..3bbd8d2046 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 3015d9338d..5b0144b386 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 02091510e5..e4162db911 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -560,6 +560,13 @@ "placeholder": "デフォルト:claude", "maxTokensLabel": "最大出力トークン", "maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。" + }, + "zooGateway": { + "account": "Zoo Code アカウント", + "signInButton": "Zoo Code にサインイン", + "signInDescription": "Zoo Gateway をアカウントで使用するにはサインインしてください", + "authenticated": "認証済み", + "authenticatedAs": "{{name}} として認証済み" } }, "checkpoints": { @@ -901,7 +908,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 4a2155aba4..6629f328ce 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -560,6 +560,13 @@ "placeholder": "기본값: claude", "maxTokensLabel": "최대 출력 토큰", "maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다." + }, + "zooGateway": { + "account": "Zoo Code 계정", + "signInButton": "Zoo Code에 로그인", + "signInDescription": "계정으로 Zoo Gateway를 사용하려면 로그인하세요", + "authenticated": "인증됨", + "authenticatedAs": "{{name}}(으)로 인증됨" } }, "checkpoints": { @@ -901,7 +908,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 006fd8721f..47b5b1bb78 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 c4492da87c..6cf50374cf 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 6e19325c5c..586a133852 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 2641939b9b..b4c058e51c 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -560,6 +560,13 @@ "placeholder": "По умолчанию: claude", "maxTokensLabel": "Макс. выходных токенов", "maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000." + }, + "zooGateway": { + "account": "Учетная запись Zoo Code", + "signInButton": "Войти в Zoo Code", + "signInDescription": "Войдите, чтобы использовать Zoo Gateway с вашей учетной записью", + "authenticated": "Аутентифицирован", + "authenticatedAs": "Аутентифицирован как {{name}}" } }, "checkpoints": { @@ -901,7 +908,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 2ed19417e7..2d00ab2eb1 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,8 @@ "providerNotAllowed": "Sağlayıcı '{{provider}}' kuruluşunuz tarafından izin verilmiyor", "modelNotAllowed": "Model '{{model}}' sağlayıcı '{{provider}}' için kuruluşunuz tarafından izin verilmiyor", "profileInvalid": "Bu profil, kuruluşunuz tarafından izin verilmeyen bir sağlayıcı veya model içeriyor", - "qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısın" + "qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısınız", + "zooGatewaySignIn": "Zoo Gateway'i kullanmak için Zoo Code'a giriş yapmalısınız. Kimlik doğrulamak için 'Giriş Yap' düğmesine tıklayın." }, "placeholders": { "apiKey": "API anahtarını girin...", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index e32beeff3e..ed8a01523c 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -560,6 +560,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": { @@ -901,7 +908,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 fb67bb89c0..2d257e1c1d 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -560,6 +560,13 @@ "placeholder": "默认:claude", "maxTokensLabel": "最大输出 Token", "maxTokensDescription": "Claude Code 响应的最大输出 Token 数量。默认为 8000。" + }, + "zooGateway": { + "account": "Zoo Code 账户", + "signInButton": "登录 Zoo Code", + "signInDescription": "登录以使用您的账户访问 Zoo Gateway", + "authenticated": "已认证", + "authenticatedAs": "已认证为 {{name}}" } }, "checkpoints": { @@ -901,7 +908,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 f0d4725cd2..b6187594a5 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -575,6 +575,13 @@ "placeholder": "預設:claude", "maxTokensLabel": "最大輸出 Token", "maxTokensDescription": "Claude Code 回應的最大輸出 Token 數量。預設為 8000。" + }, + "zooGateway": { + "account": "Zoo Code 帳戶", + "signInButton": "登入 Zoo Code", + "signInDescription": "登入以使用您的帳戶存取 Zoo Gateway", + "authenticated": "已認證", + "authenticatedAs": "已認證為 {{name}}" } }, "checkpoints": { @@ -916,7 +923,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 ce4b7a7173..8573e7137c 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -16,7 +16,12 @@ vi.mock("i18next", () => ({ }, })) -import { getModelValidationError, validateApiConfigurationExcludingModelErrors, validateBedrockArn } from "../validate" +import { + getModelValidationError, + validateApiConfiguration, + validateApiConfigurationExcludingModelErrors, + validateBedrockArn, +} from "../validate" describe("Model Validation Functions", () => { const mockRouterModels: RouterModels = { @@ -211,6 +216,75 @@ describe("Model Validation Functions", () => { expect(result).toBe("settings:validation.modelId") }) }) + + describe("Zoo Gateway validation", () => { + describe("validateApiConfiguration (welcome-view entry point)", () => { + it("returns a sign-in error when neither profile token nor Zoo auth is present", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "anthropic/claude-sonnet-4", + } + + const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false) + expect(result).toBe("settings:validation.zooGatewaySignIn") + }) + + it("returns undefined when Zoo Code auth is active without a profile token", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "anthropic/claude-sonnet-4", + } + + const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, true) + expect(result).toBeUndefined() + }) + + it("returns undefined when a profile session token is set", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "anthropic/claude-sonnet-4", + zooSessionToken: "zoo_ext_test_token", + } + + const result = validateApiConfiguration(config, mockRouterModels, allowAllOrganization, false) + expect(result).toBeUndefined() + }) + }) + + describe("validateApiConfigurationExcludingModelErrors (settings form)", () => { + // The settings form short-circuits zoo-gateway and renders the sign-in + // error inline in `ZooGateway.tsx`, so this entry point must never + // surface a zoo-gateway-specific error regardless of auth state. + it("returns undefined for zoo-gateway when unauthenticated and no token", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "anthropic/claude-sonnet-4", + } + + const result = validateApiConfigurationExcludingModelErrors( + config, + mockRouterModels, + allowAllOrganization, + ) + expect(result).toBeUndefined() + }) + + it("returns undefined for zoo-gateway when a profile token is set", () => { + const config: ProviderSettings = { + apiProvider: "zoo-gateway", + zooGatewayModelId: "anthropic/claude-sonnet-4", + zooSessionToken: "zoo_ext_test_token", + } + + const result = validateApiConfigurationExcludingModelErrors( + config, + mockRouterModels, + allowAllOrganization, + ) + expect(result).toBeUndefined() + }) + }) + }) }) describe("validateBedrockArn", () => { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 0e6e1c6b9d..cde7d4504e 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) { @@ -128,6 +132,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,12 +291,20 @@ export function getModelValidationError( * Validates API configuration but excludes model-specific errors. * This is used for the general API error display to prevent duplication * when model errors are shown in the model selector. + * + * Zoo Gateway intentionally short-circuits here — its sign-in error is rendered + * inline by the `ZooGateway` provider component so the form-level error effect + * does not need to track Zoo Code auth state. */ export function validateApiConfigurationExcludingModelErrors( apiConfiguration: ProviderSettings, _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, ): string | undefined { + if (apiConfiguration.apiProvider === "zoo-gateway") { + return undefined + } + const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) if (keysAndIdsPresentErrorMessage) {