diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d597eb7e..b3ee711ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ - **fix(claude):** drop orphan `tool_result` blocks left behind when `fixToolAdjacency` strips a dangling `tool_use` — resolves HTTP 400 "unexpected tool_use_id in tool_result blocks" from the Anthropic API on truncated histories. `fixToolPairs` now re-runs after every `fixToolAdjacency` pass across all three call sites (`contextManager.ts`, `base.ts`, `claudeCodeCompatible.ts`). (discussion [#2410](https://github.com/diegosouzapw/OmniRoute/discussions/2410)) - **fix(playground):** guard against `null`/non-string model IDs in Playground dropdowns — `typeof m?.id !== "string"` check prevents a silent crash in the provider discovery loop and `filteredModels` computation that was leaving all Playground dropdowns empty when `/v1/models` returned entries with `id: null`; adds deduplication via `Set` to eliminate duplicate React key warnings. - **fix(mitm):** point MITM runtime manager re-export to the compiled `.js` entrypoint — fixes module resolution after build when the `.ts` source is no longer present. +- **fix(storage):** persist `STORAGE_ENCRYPTION_KEY` across upgrades (closes #1622) — ensures that SQLite encryption keys are preserved during version upgrades. ([#2428](https://github.com/diegosouzapw/OmniRoute/pull/2428) — thanks @Chewji9875) +- **fix(auth):** auto-reset credential `apiKeyHealth` status on successful connection test. ([#2427](https://github.com/diegosouzapw/OmniRoute/pull/2427) — thanks @clousky2020) +- **fix(mitm):** drop `.js` extension on `manager.runtime` re-export to fix webpack packaging issue. ([#2425](https://github.com/diegosouzapw/OmniRoute/pull/2425) — thanks @NomenAK) +- **fix(image):** support Antigravity image generation and add Gemini 3.5 Flash support. ([#2423](https://github.com/diegosouzapw/OmniRoute/pull/2423) — thanks @backryun) #### 2026-05-19 diff --git a/bin/cli/commands/reset-encrypted-columns.mjs b/bin/cli/commands/reset-encrypted-columns.mjs index 165c85eb1b..2fff4394b1 100644 --- a/bin/cli/commands/reset-encrypted-columns.mjs +++ b/bin/cli/commands/reset-encrypted-columns.mjs @@ -1,11 +1,14 @@ -import { fileURLToPath } from "node:url"; -import { dirname, resolve } from "node:path"; import { existsSync } from "node:fs"; import { resolveDataDir } from "../data-dir.mjs"; import { join } from "node:path"; -const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const ENCRYPTED_PATTERN = "enc:v1:%"; +const ENCRYPTED_COLUMNS = ["api_key", "access_token", "refresh_token", "id_token"]; +/** + * Direct SQLite implementation for reset-encrypted-columns. + * Uses better-sqlite3 directly to avoid TypeScript source dependencies in production builds. + */ export async function runResetEncryptedColumns(argv) { const dataDir = resolveDataDir(); const dbPath = join(dataDir, "storage.sqlite"); @@ -38,22 +41,37 @@ export async function runResetEncryptedColumns(argv) { return 0; } + let db; try { - const { countEncryptedCredentials, resetEncryptedColumns } = await import( - `${PROJECT_ROOT}/src/lib/db/recovery.ts` + // Use createRequire to load better-sqlite3 (works in both dev and production) + const { createRequire } = await import("node:module"); + const require = createRequire(import.meta.url); + const Database = require("better-sqlite3"); + + db = new Database(dbPath); + + // Build WHERE clause for encrypted values + const whereClause = ENCRYPTED_COLUMNS.map((col) => `${col} LIKE '${ENCRYPTED_PATTERN}'`).join( + " OR " ); - const count = countEncryptedCredentials(); + // Count affected rows + const countResult = db + .prepare(`SELECT COUNT(*) AS cnt FROM provider_connections WHERE ${whereClause}`) + .get(); + const count = countResult?.cnt ?? 0; if (count === 0) { console.log("\x1b[32m✔ No encrypted credentials found — nothing to reset.\x1b[0m"); return 0; } - const { affected } = resetEncryptedColumns({ dryRun: false }); + // Reset columns + const nullCols = ENCRYPTED_COLUMNS.map((col) => `${col} = NULL`).join(", "); + db.prepare(`UPDATE provider_connections SET ${nullCols} WHERE ${whereClause}`).run(); console.log( - `\x1b[32m✔ Reset ${affected} provider connection(s).\x1b[0m\n` + + `\x1b[32m✔ Reset ${count} provider connection(s).\x1b[0m\n` + ` Re-authenticate your providers in the dashboard or re-add API keys.\n` ); return 0; @@ -62,5 +80,9 @@ export async function runResetEncryptedColumns(argv) { `\x1b[31m✖ Failed to reset encrypted columns:\x1b[0m ${err instanceof Error ? err.message : String(err)}` ); return 1; + } finally { + if (db) { + db.close(); + } } } diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index 43702422e3..e4e8c9e89f 100644 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -67,6 +67,47 @@ function loadEnvFile() { loadEnvFile(); +// Generate STORAGE_ENCRYPTION_KEY if not set (persisted to ~/.omniroute/.env) +// This ensures the key survives across upgrades and is not regenerated on each install. +// See: https://github.com/diegosouzapw/OmniRoute/issues/1622 +{ + const { randomBytes } = await import("node:crypto"); + const { existsSync, mkdirSync, readFileSync, writeFileSync } = await import("node:fs"); + const { join } = await import("node:path"); + const { homedir } = await import("node:os"); + + if (!process.env.STORAGE_ENCRYPTION_KEY) { + const dataDir = join(homedir(), ".omniroute"); + const envPath = join(dataDir, ".env"); + + // Ensure data directory exists + if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); + } + + const key = randomBytes(32).toString("hex"); + + // Read existing .env content or start fresh + let content = ""; + if (existsSync(envPath)) { + content = readFileSync(envPath, "utf-8"); + } + + // Append key if not already present + if (!content.includes("STORAGE_ENCRYPTION_KEY=")) { + const separator = content.trim() ? "\n" : ""; + const newContent = content.trimEnd() + separator + `STORAGE_ENCRYPTION_KEY=${key}`; + writeFileSync(envPath, newContent + "\n", "utf-8"); + console.log( + " \x1b[2m✨ Generated STORAGE_ENCRYPTION_KEY in ~/.omniroute/.env\x1b[0m" + ); + } + + // Set in process.env for immediate use + process.env.STORAGE_ENCRYPTION_KEY = key; + } +} + // Apply --lang before Commander parses (program descriptions call t() during setup) { const langIdx = process.argv.findIndex((a) => a === "--lang"); diff --git a/open-sse/config/antigravityModelAliases.ts b/open-sse/config/antigravityModelAliases.ts index e01375bb12..45c6e8d079 100644 --- a/open-sse/config/antigravityModelAliases.ts +++ b/open-sse/config/antigravityModelAliases.ts @@ -1,12 +1,11 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([ { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" }, { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 Thinking" }, - { id: "gemini-3-pro-preview", name: "Gemini 3.1 Pro (High)" }, + { id: "gemini-3-flash-agent", name: "Gemini 3.5 Flash (High)" }, + { id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)" }, + { id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" }, { id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" }, - { id: "gemini-3-flash-preview", name: "Gemini 3 Flash" }, { id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" }, - { id: "gemini-3-pro-image-preview", name: "Gemini 3 Pro Image" }, - { id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash Image" }, { id: "gemini-2.5-computer-use-preview-10-2025", name: "Gemini 2.5 Computer Use Preview (10/2025)", diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 72fd474609..a32ce55638 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -37,6 +37,12 @@ interface ImageModelAliasEntry { } const IMAGE_MODEL_ALIASES: Record = { + "gemini-3.1-flash-image-preview": { + provider: "antigravity", + model: "gemini-3.1-flash-image", + name: "Gemini 3.1 Flash Image", + listInCatalog: false, + }, "flux-kontext": { provider: "black-forest-labs", model: "flux-kontext-pro", @@ -223,11 +229,11 @@ export const IMAGE_PROVIDERS: Record = { antigravity: { id: "antigravity", - baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", + baseUrl: "https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent", authType: "oauth", authHeader: "bearer", format: "gemini-image", // Special format: uses Gemini generateContent API - models: [], + models: [{ id: "gemini-3.1-flash-image", name: "Gemini 3.1 Flash Image" }], supportedSizes: ["1024x1024"], }, diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 065696870e..6fa9f258d8 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -1,7 +1,11 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts"; import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts"; import { supportsXHighEffort } from "../config/providerModels.ts"; -import { getRotatingApiKey, getValidApiKey } from "../services/apiKeyRotator.ts"; +import { + getRotatingApiKey, + getValidApiKey, + resolveKeyForRequest, +} from "../services/apiKeyRotator.ts"; import type { KeyHealth } from "../services/apiKeyRotator.ts"; import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts"; import type { ProviderRequestDefaults } from "../services/providerRequestDefaults.ts"; @@ -345,22 +349,25 @@ export class BaseExecutor { if (credentials.accessToken) { headers["Authorization"] = `Bearer ${credentials.accessToken}`; } else if (credentials.apiKey) { - // T07: rotate between primary + extra API keys when extraApiKeys is configured const extraKeys = (credentials.providerSpecificData?.extraApiKeys as string[] | undefined) ?? []; - // Extract health directly from credentials for reliability across all call paths - const credentialsHealth = - health ?? - (credentials.providerSpecificData?.apiKeyHealth as Record | undefined); - const effectiveKey = - extraKeys.length > 0 && credentials.connectionId - ? getValidApiKey( - credentials.connectionId, - credentials.apiKey, - extraKeys, - credentialsHealth - ) || credentials.apiKey - : credentials.apiKey; + const selectedKeyId = ( + credentials.providerSpecificData as Record | undefined + )?.selectedKeyId as string | undefined; + let effectiveKey = credentials.apiKey; + if (extraKeys.length > 0 && credentials.connectionId) { + const resolved = resolveKeyForRequest( + credentials.connectionId, + credentials.apiKey, + extraKeys, + selectedKeyId ?? null + ); + effectiveKey = resolved?.key ?? credentials.apiKey; + if (resolved && credentials.providerSpecificData) { + (credentials.providerSpecificData as Record).selectedKeyId = + resolved.keyId; + } + } headers["Authorization"] = `Bearer ${effectiveKey}`; } diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 1838a4664d..ee192f7cf4 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -1,7 +1,11 @@ import { BaseExecutor } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; -import { getRotatingApiKey, getValidApiKey } from "../services/apiKeyRotator.ts"; +import { + getRotatingApiKey, + getValidApiKey, + resolveKeyForRequest, +} from "../services/apiKeyRotator.ts"; import type { KeyHealth } from "../services/apiKeyRotator.ts"; import { buildClaudeCodeCompatibleHeaders, @@ -254,14 +258,22 @@ export class DefaultExecutor extends BaseExecutor { // T07: resolve extra keys round-robin locally since DefaultExecutor overrides BaseExecutor buildHeaders const extraKeys = (credentials.providerSpecificData?.extraApiKeys as string[] | undefined) ?? []; - const health = credentials.providerSpecificData?.apiKeyHealth as - | Record - | undefined; - const effectiveKey = - extraKeys.length > 0 && credentials.connectionId && credentials.apiKey - ? getValidApiKey(credentials.connectionId, credentials.apiKey, extraKeys, health) || - credentials.apiKey - : credentials.apiKey; + const selectedKeyId = (credentials.providerSpecificData as Record | undefined) + ?.selectedKeyId as string | undefined; + let effectiveKey = credentials.apiKey; + if (extraKeys.length > 0 && credentials.connectionId && credentials.apiKey) { + const resolved = resolveKeyForRequest( + credentials.connectionId, + credentials.apiKey, + extraKeys, + selectedKeyId ?? null + ); + effectiveKey = resolved?.key ?? credentials.apiKey; + if (resolved && credentials.providerSpecificData) { + (credentials.providerSpecificData as Record).selectedKeyId = + resolved.keyId; + } + } switch (this.provider) { case "gemini": diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index e5041adb5d..5060b72b8c 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -49,7 +49,6 @@ import { updateProviderConnection } from "@/lib/db/providers"; import { recordKeyFailure, recordKeySuccess, - getLastUsedKeyId, getInvalidKeyCount, trackConnectionExtraKeys, connectionHasExtraKeys, @@ -1319,6 +1318,63 @@ export async function handleChatCore({ }).catch(() => {}); }; + const recordKeyHealthStatus = ( + status: number, + creds: Record | null | undefined + ): void => { + const connId = creds?.connectionId as string | undefined; + if (!connId) return; + + const psd = creds.providerSpecificData as Record | undefined; + const extraKeys = (psd?.extraApiKeys as string[] | undefined) ?? []; + const health = psd?.apiKeyHealth as Record | undefined; + const currentKeyId = (psd?.selectedKeyId as string | undefined) ?? "primary"; + + trackConnectionExtraKeys(connId, extraKeys); + + if (status === 401) { + const updatedHealth = recordKeyFailure(connId, currentKeyId); + log?.warn?.( + "AUTH", + `401 on connection ${connId.slice(0, 8)} - key marked as failed (failure #${updatedHealth.failures})` + ); + + // Persist health status to DB on every failure (not just invalid transitions) + // This ensures in-memory state survives process restarts + const prevStatus = health?.[currentKeyId]?.status; + const prevFailures = health?.[currentKeyId]?.failures ?? 0; + if (updatedHealth.status !== prevStatus || updatedHealth.failures !== prevFailures) { + updateProviderConnection(connId, { + providerSpecificData: { + ...psd, + apiKeyHealth: { ...health, [currentKeyId]: updatedHealth }, + }, + }).catch((err: unknown) => { + log?.error?.( + "DB", + `Failed to persist apiKeyHealth: ${err instanceof Error ? err.message : String(err)}` + ); + }); + } + } else if (status >= 200 && status < 300) { + const updatedHealth = recordKeySuccess(connId, currentKeyId); + const prevStatus = health?.[currentKeyId]?.status; + if (prevStatus === "warning" || prevStatus === "invalid") { + updateProviderConnection(connId, { + providerSpecificData: { + ...psd, + apiKeyHealth: { ...health, [currentKeyId]: updatedHealth }, + }, + }).catch((err: unknown) => { + log?.error?.( + "DB", + `Failed to persist apiKeyHealth: ${err instanceof Error ? err.message : String(err)}` + ); + }); + } + } + }; + const persistCodexQuotaState = async ( headers: Headers | Record | null, status = 0 @@ -3029,6 +3085,8 @@ export async function handleChatCore({ const executeProviderRequest = async (modelToCall = effectiveModel, allowDedup = false) => { const execute = async () => { const executionCredentials = getExecutionCredentials(); + // Track execution credentials for key health recording (to capture selectedKeyId) + let lastExecCreds = executionCredentials; const accountSemaphoreMaxConcurrency = resolveAccountSemaphoreMaxConcurrency(executionCredentials); const accountSemaphoreKey = resolveAccountSemaphoreKey({ @@ -3160,11 +3218,12 @@ export async function handleChatCore({ while (attempts < maxAttempts) { trace("pre_executor", { attempt: attempts }); + const execCreds = getExecutionCredentials(); const res = await executor.execute({ model: modelToCall, body: bodyToSend, stream: upstreamStream, - credentials: getExecutionCredentials(), + credentials: execCreds, signal: streamController.signal, log, extendedContext, @@ -3175,41 +3234,8 @@ export async function handleChatCore({ }); trace("post_executor", { status: res?.response?.status }); - // T07: Handle 401 authentication errors with API key health tracking - if (res.response.status === 401 && credentials?.connectionId) { - const psd = credentials.providerSpecificData as Record | undefined; - const extraKeys = (psd?.extraApiKeys as string[] | undefined) ?? []; - const health = psd?.apiKeyHealth as Record | undefined; - - // Track extra keys for A3 guard (prevents disabling entire connection on single-key failure) - trackConnectionExtraKeys(credentials.connectionId, extraKeys); - - const currentKeyId = getLastUsedKeyId(credentials.connectionId) || "primary"; - - // Record failure for the current key - const updatedHealth = recordKeyFailure(credentials.connectionId, currentKeyId); - log?.warn?.( - "AUTH", - `401 on connection ${credentials.connectionId.slice(0, 8)} - key marked as failed (${updatedHealth.failures}/${3})` - ); - - // Persist health status to DB if key is now invalid - if ( - updatedHealth.status === "invalid" && - health?.[currentKeyId]?.status !== "invalid" - ) { - updateProviderConnection(credentials.connectionId, { - providerSpecificData: { - ...psd, - apiKeyHealth: { ...health, [currentKeyId]: updatedHealth }, - }, - }).catch((err) => { - log?.error?.( - "DB", - `Failed to persist apiKeyHealth: ${err instanceof Error ? err.message : String(err)}` - ); - }); - } + if (res.response.status === 401 && execCreds?.connectionId) { + recordKeyHealthStatus(401, execCreds); } // Qwen 429 strict quota backoff (wait 1.5s, 3s and retry) @@ -3337,6 +3363,7 @@ export async function handleChatCore({ return { ...res, + _executionCredentials: execCreds, response: new Response( wrapReadableStreamWithFinalize(originalBody, acquireAccountSemaphoreRelease), { @@ -3349,7 +3376,10 @@ export async function handleChatCore({ }; } - return res; + return { + ...res, + _executionCredentials: execCreds, + }; } }, streamController.signal @@ -3362,62 +3392,12 @@ export async function handleChatCore({ // Non-stream: release semaphore immediately after reading full response body. const status = rawResult.response.status; - // T07: Record API key health status - if (credentials?.connectionId && credentials?.apiKey) { - const psd = credentials.providerSpecificData as Record | undefined; - const extraKeys = (psd?.extraApiKeys as string[] | undefined) ?? []; - const health = psd?.apiKeyHealth as Record | undefined; - - if (status === 401) { - // Track extra keys for A3 guard (prevents disabling entire connection on single-key failure) - trackConnectionExtraKeys(credentials.connectionId, extraKeys); - - // Authentication failed - mark current key as failed - const currentKeyId = getLastUsedKeyId(credentials.connectionId) || "primary"; - const updatedHealth = recordKeyFailure(credentials.connectionId, currentKeyId); - log?.warn?.( - "AUTH", - `401 on connection ${credentials.connectionId.slice(0, 8)} - key marked as failed (${updatedHealth.failures}/3)` - ); - - // Persist to DB if status changed to invalid - if ( - updatedHealth.status === "invalid" && - health?.[currentKeyId]?.status !== "invalid" - ) { - updateProviderConnection(credentials.connectionId, { - providerSpecificData: { - ...psd, - apiKeyHealth: { ...health, [currentKeyId]: updatedHealth }, - }, - }).catch((err) => { - log?.error?.( - "DB", - `Failed to persist apiKeyHealth: ${err instanceof Error ? err.message : String(err)}` - ); - }); - } - } else if (status >= 200 && status < 300) { - // Success - mark current key as successful - const currentKeyId = getLastUsedKeyId(credentials.connectionId) || "primary"; - const updatedHealth = recordKeySuccess(credentials.connectionId, currentKeyId); - - // Persist to DB if status was warning/invalid and now active - const prevStatus = health?.[currentKeyId]?.status; - if (prevStatus === "warning" || prevStatus === "invalid") { - updateProviderConnection(credentials.connectionId, { - providerSpecificData: { - ...psd, - apiKeyHealth: { ...health, [currentKeyId]: updatedHealth }, - }, - }).catch((err) => { - log?.error?.( - "DB", - `Failed to persist apiKeyHealth: ${err instanceof Error ? err.message : String(err)}` - ); - }); - } - } + // Use execution credentials captured during request processing + if ( + rawResult._executionCredentials?.connectionId && + rawResult._executionCredentials?.apiKey + ) { + recordKeyHealthStatus(status, rawResult._executionCredentials); } const statusText = rawResult.response.statusText; @@ -3747,7 +3727,13 @@ export async function handleChatCore({ } else if (errorType === PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED) { // Plan A: if connection has extra API keys, don't disable — only the failing key is affected. // Single-key connections still get disabled as before. - if (connectionHasExtraKeys(connectionId)) { + if ( + connectionHasExtraKeys( + connectionId, + (credentials?.providerSpecificData as Record | undefined) + ?.extraApiKeys as string[] | undefined + ) + ) { await updateProviderConnection(connectionId, { lastErrorType: errorType, lastError: message, diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 37558735e6..e6cd541657 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -17,6 +17,14 @@ import { randomUUID } from "crypto"; */ import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts"; +import { HTTP_STATUS } from "../config/constants.ts"; +import { antigravityUserAgent } from "../services/antigravityHeaders.ts"; +import { + deriveAntigravityMachineId, + getAntigravityEnvelopeUserAgent, + getAntigravityVscodeSessionId, +} from "../services/antigravityIdentity.ts"; +import { getCachedAntigravityVersion } from "../services/antigravityVersion.ts"; import { kieExecutor } from "../executors/kie.ts"; import { mapImageSize } from "../translator/image/sizeMapper.ts"; import { getCodexClientVersion, getCodexUserAgent } from "../config/codexClient.ts"; @@ -38,7 +46,7 @@ import { extractComfyOutputFiles, } from "../utils/comfyuiClient.ts"; import { fetchRemoteImage } from "@/shared/network/remoteImageFetch"; -import { sanitizeErrorMessage } from "../utils/error.ts"; +import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../utils/error.ts"; interface KieImageOptions { model: string; @@ -81,6 +89,32 @@ const OPENAI_IMAGE_TO_IMAGE_MODELS = new Set([ "qwen-image", ]); +const IMAGE_ASPECT_RATIO_PATTERN = /^\d+:\d+$/; + +function normalizeImageAspectRatio(value: unknown, fallbackSize: unknown): string { + if (typeof value === "string") { + const trimmedValue = value.trim(); + if (IMAGE_ASPECT_RATIO_PATTERN.test(trimmedValue)) return trimmedValue; + } + return mapImageSize(typeof fallbackSize === "string" ? fallbackSize : null); +} + +function parseJsonOrNull(value: string): unknown | null { + try { + return JSON.parse(value); + } catch { + return null; + } +} + +function sanitizeImageProviderError(errorText: string): unknown { + const parsed = parseJsonOrNull(errorText); + if (parsed !== null) { + return sanitizeUpstreamDetails(parsed) || sanitizeErrorMessage(errorText); + } + return sanitizeErrorMessage(errorText); +} + const BFL_MODEL_ENDPOINTS = { "flux-2-max": "/v1/flux-2-max", "flux-2-pro": "/v1/flux-2-pro", @@ -602,42 +636,79 @@ async function handleKieImageGeneration({ */ async function handleGeminiImageGeneration({ model, providerConfig, body, credentials, log }) { const startTime = Date.now(); - const url = `${providerConfig.baseUrl}/${model}:generateContent`; + const url = providerConfig.baseUrl; const provider = "antigravity"; + const credentialRecord = credentials || {}; + const token = credentialRecord.accessToken || credentialRecord.apiKey; + const providerSpecificData = credentialRecord.providerSpecificData; + const providerSpecificProjectId = + providerSpecificData && typeof providerSpecificData === "object" + ? (providerSpecificData as Record).projectId + : null; + const credentialProjectId = + typeof credentialRecord.projectId === "string" ? credentialRecord.projectId.trim() : ""; + const providerProjectId = + typeof providerSpecificProjectId === "string" ? providerSpecificProjectId.trim() : ""; + const projectId = credentialProjectId || providerProjectId || null; + const candidateCount = + typeof body.n === "number" && Number.isFinite(body.n) && body.n > 0 ? Math.floor(body.n) : 1; + const promptText = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? ""); // Summarized request for call log const logRequestBody = { model: body.model, - prompt: - typeof body.prompt === "string" - ? body.prompt.slice(0, 200) - : String(body.prompt ?? "").slice(0, 200), + prompt: promptText.slice(0, 200), size: body.size || "default", - n: body.n || 1, + n: candidateCount, }; - const geminiBody = { - contents: [ - { - parts: [{ text: body.prompt }], + if (!projectId || typeof projectId !== "string") { + return saveImageErrorResult({ + provider, + model, + status: 400, + startTime, + error: + "Missing Google projectId for Antigravity account. Please reconnect OAuth in Providers so OmniRoute can fetch your Cloud Code project.", + requestBody: logRequestBody, + }); + } + + const antigravityBody = { + project: projectId, + requestId: `image_gen/${Date.now()}/${randomUUID()}/0`, + request: { + contents: [ + { + role: "user", + parts: [{ text: promptText }], + }, + ], + generationConfig: { + candidateCount, + imageConfig: { + aspectRatio: normalizeImageAspectRatio(body.aspect_ratio, body.size), + }, }, - ], - generationConfig: { - responseModalities: ["TEXT", "IMAGE"], }, + model, + userAgent: getAntigravityEnvelopeUserAgent(credentialRecord), + requestType: "image_gen", }; - const token = credentials.accessToken || credentials.apiKey; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${token}`, + "User-Agent": antigravityUserAgent(), + "x-client-name": "antigravity", + "x-client-version": getCachedAntigravityVersion(), + "x-machine-id": deriveAntigravityMachineId(credentialRecord), + "x-vscode-sessionid": getAntigravityVscodeSessionId(), + "x-goog-user-project": projectId, }; if (log) { - const promptPreview = - typeof body.prompt === "string" - ? body.prompt.slice(0, 60) - : String(body.prompt ?? "").slice(0, 60); + const promptPreview = promptText.slice(0, 60); log.info( "IMAGE", `antigravity/${model} (gemini) | prompt: "${promptPreview}..." | format: gemini-image` @@ -645,16 +716,32 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden } try { - const response = await fetch(url, { + let response = await fetch(url, { method: "POST", headers, - body: JSON.stringify(geminiBody), + body: JSON.stringify(antigravityBody), }); + if (response.status === HTTP_STATUS.FORBIDDEN && headers["x-goog-user-project"]) { + const retryHeaders = { ...headers }; + delete retryHeaders["x-goog-user-project"]; + if (log) { + log.info("IMAGE", "antigravity image 403 with x-goog-user-project; retrying without it"); + } + response = await fetch(url, { + method: "POST", + headers: retryHeaders, + body: JSON.stringify(antigravityBody), + }); + } + if (!response.ok) { const errorText = await response.text(); + const safeError = sanitizeImageProviderError(errorText); + const safeErrorLog = + typeof safeError === "string" ? safeError : JSON.stringify(safeError ?? {}); if (log) { - log.error("IMAGE", `antigravity error ${response.status}: ${errorText.slice(0, 200)}`); + log.error("IMAGE", `antigravity error ${response.status}: ${safeErrorLog.slice(0, 200)}`); } saveCallLog({ @@ -664,25 +751,26 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden model: `antigravity/${model}`, provider, duration: Date.now() - startTime, - error: errorText.slice(0, 500), + error: safeErrorLog.slice(0, 500), requestBody: logRequestBody, }).catch(() => {}); - return { success: false, status: response.status, error: errorText }; + return { success: false, status: response.status, error: safeError }; } const data = await response.json(); + const responseBody = data.response || data; - // Extract image data from Gemini response + // Extract image data from Antigravity's wrapped Gemini response. const images = []; - const candidates = data.candidates || []; + const candidates = responseBody.candidates || []; for (const candidate of candidates) { const parts = candidate.content?.parts || []; for (const part of parts) { if (part.inlineData) { images.push({ b64_json: part.inlineData.data, - revised_prompt: parts.find((p) => p.text)?.text || body.prompt, + revised_prompt: parts.find((p) => p.text)?.text || promptText, }); } } @@ -726,7 +814,7 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -1306,7 +1394,7 @@ async function handleFalAIImageGeneration({ model, status: 502, startTime, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }); } } @@ -1496,7 +1584,7 @@ async function handleStabilityAIImageGeneration({ model, status: 502, startTime, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }); } } @@ -1615,7 +1703,7 @@ async function handleBlackForestLabsImageGeneration({ model, status: 502, startTime, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }); } } @@ -1690,7 +1778,7 @@ async function handleRecraftImageGeneration({ model, status: 502, startTime, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }); } } @@ -1778,7 +1866,7 @@ async function handleTopazImageGeneration({ model, status: 502, startTime, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }); } } @@ -2358,7 +2446,7 @@ async function fetchImageEndpoint(url, headers, body, provider, log) { return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -2456,7 +2544,7 @@ async function handleHyperbolicImageGeneration({ return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -2714,7 +2802,7 @@ async function handleNanoBananaImageGeneration({ return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -2910,7 +2998,7 @@ async function handleSDWebUIImageGeneration({ model, provider, providerConfig, b return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -3016,7 +3104,7 @@ async function handleComfyUIImageGeneration({ model, provider, providerConfig, b return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -3128,7 +3216,7 @@ async function handleHaiperImageGeneration({ return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -3260,7 +3348,7 @@ async function handleLeonardoImageGeneration({ return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } @@ -3350,7 +3438,7 @@ async function handleIdeogramImageGeneration({ return { success: false, status: 502, - error: sanitizeErrorMessage(err) || "Image provider error", + error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`, }; } } diff --git a/open-sse/services/apiKeyRotator.ts b/open-sse/services/apiKeyRotator.ts index 6afeb33065..1814fb833d 100644 --- a/open-sse/services/apiKeyRotator.ts +++ b/open-sse/services/apiKeyRotator.ts @@ -18,10 +18,6 @@ // In-memory round-robin index per connection const _keyIndexes = new Map(); -// Tracks the last keyId selected by getValidApiKey() for a connection. -// Used by chatCore.ts to know which key to record failures/successes against. -const _lastUsedKeyId = new Map(); - // Tracks which connections have extra API keys (for A3 guard in chatCore.ts) // Used to prevent disabling an entire connection when only one key fails. const _connectionExtraKeys = new Map(); @@ -60,7 +56,7 @@ interface KeyHealth { const _keyHealth = new Map(); -const FAILURE_THRESHOLD = 3; // Mark as invalid after 3 consecutive failures +const FAILURE_THRESHOLD = 2; // Mark as invalid after 2 consecutive failures /** * Get or create health status for a specific key within a connection scope. @@ -95,7 +91,7 @@ export function getValidApiKey( primaryKey: string, extraKeys: string[] = [], health?: Record -): string | null { +): { key: string; keyId: string } | null { const validExtras = extraKeys.filter((k) => typeof k === "string" && k.trim().length > 0); // Build list of all keys with their IDs @@ -106,6 +102,10 @@ export function getValidApiKey( const primaryHealth = health?.["primary"] || getOrCreateHealth(connectionId, "primary"); if (primaryHealth.status !== "invalid") { allKeys.push({ key: primaryKey, keyId: "primary" }); + } else { + console.warn( + `[KeyRotator] Skipping invalid primary key for connection ${connectionId.slice(0, 8)}` + ); } } @@ -120,8 +120,7 @@ export function getValidApiKey( if (allKeys.length === 0) return null; if (allKeys.length === 1) { - _lastUsedKeyId.set(connectionId, allKeys[0].keyId); - return allKeys[0].key; + return { key: allKeys[0].key, keyId: allKeys[0].keyId }; } // Round-robin among valid keys only @@ -129,16 +128,7 @@ export function getValidApiKey( const idx = current % allKeys.length; _keyIndexes.set(connectionId, current + 1); - _lastUsedKeyId.set(connectionId, allKeys[idx].keyId); - return allKeys[idx].key; -} - -/** - * Get the keyId that was last selected by getValidApiKey() for a connection. - * Used by chatCore.ts to record failures/successes against the correct key. - */ -export function getLastUsedKeyId(connectionId: string): string | undefined { - return _lastUsedKeyId.get(connectionId); + return { key: allKeys[idx].key, keyId: allKeys[idx].keyId }; } /** @@ -299,4 +289,53 @@ export function getApiKeyCount(primaryKey: string, extraKeys: string[] = []): nu return (primaryKey ? 1 : 0) + validExtras.length; } +/** + * Resolve the API key and its health status for an ongoing request. + * + * Unlike getValidApiKey() (which does round-robin for every call), this + * method re-uses the previously selected keyId when available — ensuring + * that a multi-turn request stream keeps using the same key. If no key + * was selected yet or the stored key is no longer valid, it falls back + * to fresh round-robin via getValidApiKey(). + * + * @returns The resolved key+keyId, or null if no valid keys remain. + */ +export function resolveKeyForRequest( + connectionId: string, + primaryKey: string, + extraKeys: string[], + selectedKeyId: string | null +): { key: string; keyId: string } | null { + if (selectedKeyId) { + const health = getOrCreateHealth(connectionId, selectedKeyId); + if (health.status !== "invalid") { + if (selectedKeyId === "primary" && primaryKey) { + return { key: primaryKey, keyId: "primary" }; + } + const match = /^extra_(\d+)$/.exec(selectedKeyId); + if (match) { + const idx = Number.parseInt(match[1], 10); + if (idx >= 0 && idx < extraKeys.length && extraKeys[idx].trim().length > 0) { + return { key: extraKeys[idx], keyId: selectedKeyId }; + } + } + } + } + + return getValidApiKey(connectionId, primaryKey, extraKeys); +} + +export function removeConnectionHealth(connectionId: string): void { + for (const key of _keyHealth.keys()) { + if (key.startsWith(`${connectionId}:`)) { + _keyHealth.delete(key); + } + } +} + +export function removeConnectionIndex(connectionId: string): void { + _keyIndexes.delete(connectionId); + _connectionExtraKeys.delete(connectionId); +} + export type { KeyHealth }; diff --git a/scripts/dev/sync-env.mjs b/scripts/dev/sync-env.mjs index 050282afb7..f033319be3 100644 --- a/scripts/dev/sync-env.mjs +++ b/scripts/dev/sync-env.mjs @@ -24,16 +24,23 @@ const require = createRequire(import.meta.url); const CRYPTO_SECRETS = { JWT_SECRET: () => randomBytes(64).toString("hex"), API_KEY_SECRET: () => randomBytes(32).toString("hex"), - STORAGE_ENCRYPTION_KEY: () => randomBytes(32).toString("hex"), + // STORAGE_ENCRYPTION_KEY: Generated at server startup instead of postinstall. + // Generated in bin/omniroute.mjs:ensureStorageEncryptionKey() and persisted to + // ~/.omniroute/.env to survive across upgrades. This prevents credential loss + // when upgrading OmniRoute (issue #1622). MACHINE_ID_SALT: () => `omniroute-${randomBytes(8).toString("hex")}`, }; /** * Keys that MUST NOT be regenerated when existing encrypted data exists in the DB. * Generating a new key would make all previously-encrypted credentials unrecoverable. + * + * Note: STORAGE_ENCRYPTION_KEY is no longer auto-generated in postinstall. + * It's generated at server startup in bin/omniroute.mjs and persisted to + * ~/.omniroute/.env to survive across upgrades. * @see https://github.com/diegosouzapw/OmniRoute/issues/1622 */ -const ENCRYPTION_BOUND_KEYS = new Set(["STORAGE_ENCRYPTION_KEY"]); +const ENCRYPTION_BOUND_KEYS = new Set([]); // ── Resolve DATA_DIR (mirrors bootstrap-env.mjs / dataPaths.ts) ───────────── function resolveDataDir(env = process.env) { diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index 7afcfe02a9..e419e83b67 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -136,13 +136,15 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { fetchData(); }, [fetchData]); - // T07: Check for invalid API keys and show notification (once per session) - const notifiedInvalidKeys = useRef>(new Set()); + // T07: Check for unhealthy API keys and show notification (once per session) + const notifiedUnhealthyKeys = useRef>(new Set()); useEffect(() => { const checkApiKeyHealth = () => { - const newInvalidKeys = new Set(); - const invalidConnections: string[] = []; - let firstInvalidProviderId: string | null = null; + const newUnhealthyKeys = new Set(); + const unhealthyProviderIds = new Set(); + const unhealthyConnections: string[] = []; + let firstUnhealthyProviderId: string | null = null; + let hasWarning = false; for (const conn of providerConnections) { const health = conn.providerSpecificData?.apiKeyHealth as @@ -157,41 +159,63 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { | undefined; if (!health) continue; - const invalidKeys = Object.entries(health).filter(([_, h]) => h.status === "invalid"); + // Defense-in-depth: skip stale extra_N health entries whose index + // is out of range of the current extraApiKeys list. + // The backend cleans this up on PATCH, but existing stale data from + // before the fix or other code paths could still have orphan entries. + const extras: string[] = conn.providerSpecificData?.extraApiKeys ?? []; + const extraKeyCount = Array.isArray(extras) ? extras.length : 0; - if (invalidKeys.length > 0) { - for (const [keyId] of invalidKeys) { - newInvalidKeys.add(`${conn.id}:${keyId}`); + const unhealthyKeys = Object.entries(health).filter(([keyId, h]) => { + if (h.status !== "invalid" && h.status !== "warning") return false; + // extra_N entries: only flag if the index is still within bounds + if (keyId.startsWith("extra_")) { + const idx = parseInt(keyId.slice(6), 10); + if (isNaN(idx) || idx >= extraKeyCount) return false; } - if (firstInvalidProviderId === null) { - firstInvalidProviderId = conn.provider; + return true; + }); + + if (unhealthyKeys.length > 0) { + for (const [, h] of unhealthyKeys) { + if (h.status === "warning") hasWarning = true; + break; } - invalidConnections.push(conn.name || conn.id); + for (const [keyId] of unhealthyKeys) { + newUnhealthyKeys.add(`${conn.id}:${keyId}`); + } + if (firstUnhealthyProviderId === null) { + firstUnhealthyProviderId = conn.provider; + } + unhealthyConnections.push(conn.name || conn.id); + unhealthyProviderIds.add(conn.provider); } } - // Only notify for newly invalid keys (not already notified) - const hasNewInvalid = Array.from(newInvalidKeys).some( - (k) => !notifiedInvalidKeys.current.has(k) + // Only notify for newly unhealthy keys (not already notified) + const hasNewUnhealthy = Array.from(newUnhealthyKeys).some( + (k) => !notifiedUnhealthyKeys.current.has(k) ); - if (hasNewInvalid) { + if (hasNewUnhealthy) { const navigateTo = - newInvalidKeys.size === 1 && firstInvalidProviderId - ? `/dashboard/providers/${firstInvalidProviderId}` - : "/dashboard/providers"; + newUnhealthyKeys.size === 1 && firstUnhealthyProviderId + ? `/dashboard/providers/${firstUnhealthyProviderId}` + : `/dashboard/providers?search=${encodeURIComponent(Array.from(unhealthyProviderIds).join(" "))}`; + + const notificationType = hasWarning ? "warning" : "error"; useNotificationStore.getState().addNotification({ - type: "warning", - message: tp("apiKeyInvalidAlert", { - count: newInvalidKeys.size, - connections: invalidConnections.join(", "), + type: notificationType, + message: tp(hasWarning ? "apiKeyWarningAlert" : "apiKeyInvalidAlert", { + count: newUnhealthyKeys.size, + connections: unhealthyConnections.join(", "), }), - title: tp("apiKeyInvalidAlertTitle"), + title: tp(hasWarning ? "apiKeyWarningAlertTitle" : "apiKeyInvalidAlertTitle"), duration: 10000, onClick: () => router.push(navigateTo), }); - // Mark all current invalid keys as notified - newInvalidKeys.forEach((k) => notifiedInvalidKeys.current.add(k)); + // Mark all current unhealthy keys as notified + newUnhealthyKeys.forEach((k) => notifiedUnhealthyKeys.current.add(k)); } }; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index a9726a27be..768825e6b3 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -9759,7 +9759,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec ? "text-red-400" : health?.status === "warning" ? "text-yellow-400" - : "text-green-400"; + : "text-text-muted"; const statusIcon = health?.status === "invalid" ? "🔴" : health?.status === "warning" ? "🟡" : "🟢"; const statusLabel = @@ -9772,7 +9772,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec return (
{statusIcon} {t("primaryKey")}: {connection.apiKey.slice(0, 6)}... {connection.apiKey.slice(-4)} @@ -9826,7 +9826,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec ? "text-red-400" : health?.status === "warning" ? "text-yellow-400" - : "text-green-400"; + : "text-text-muted"; const statusIcon = health?.status === "invalid" ? "🔴" @@ -9843,7 +9843,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec return (
{statusIcon}{" "} {t("extraApiKeyMasked", { diff --git a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx index 60710bdfed..c1bb12c693 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx @@ -17,6 +17,7 @@ interface ProviderStats { total?: number; connected?: number; error?: number; + warning?: number; errorCode?: string | null; errorTime?: string | null; allDisabled?: boolean; @@ -57,6 +58,7 @@ const DOT_COLORS: Record = { function getStatusDisplay( connected: number, error: number, + warning: number, errorCode: string | null | undefined, t: ReturnType, afterConnected?: ReactNode @@ -70,6 +72,13 @@ function getStatusDisplay( ); if (afterConnected) parts.push(afterConnected); } + if (warning > 0) { + parts.push( + + {t("warningCount", { count: warning })} + + ); + } if (error > 0) { const errText = errorCode ? t("errorCount", { count: error, code: errorCode }) @@ -206,7 +215,14 @@ export default function ProviderCard({ ) : ( <> - {getStatusDisplay(connected, error, stats.errorCode, t, codexFastChip)} + {getStatusDisplay( + connected, + error, + Number(stats.warning || 0), + stats.errorCode, + t, + codexFastChip + )} {stats.expiryStatus === "expired" && ( {t("expiredBadge")} diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index 1b579c0d62..40c9a3727c 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -22,7 +22,7 @@ import { isClaudeCodeCompatibleProvider, CLOUD_AGENT_PROVIDERS, } from "@/shared/constants/providers"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { getErrorCode, getRelativeTime } from "@/shared/utils"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; @@ -142,12 +142,20 @@ export default function ProvidersPage() { const tc = useTranslations("common"); const ccCompatibleLabel = t("ccCompatibleLabel"); const addCcCompatibleLabel = t("addCcCompatible"); + const searchParams = useSearchParams(); useEffect(() => { setShowConfiguredOnly(readConfiguredOnlyPreference()); setConfiguredOnlyPreferenceReady(true); }, []); + useEffect(() => { + const searchFromUrl = searchParams.get("search"); + if (searchFromUrl) { + setSearchQuery(searchFromUrl); + } + }, [searchParams]); + useEffect(() => { const fetchData = async () => { try { @@ -282,9 +290,19 @@ export default function ProvidersPage() { getCodexEffectiveFastServiceTier(connection.providerSpecificData, false) )); + // Count API keys in "warning" state across all connections + const warning = providerConnections.reduce((warnCount, conn) => { + const health = (conn as any).providerSpecificData?.apiKeyHealth as + | Record + | undefined; + if (!health) return warnCount; + return warnCount + Object.values(health).filter((h) => h.status === "warning").length; + }, 0); + return { connected, error, + warning, total, errorCode, errorTime, diff --git a/src/app/api/providers/[id]/route.ts b/src/app/api/providers/[id]/route.ts index e66555f64f..980cf666a2 100644 --- a/src/app/api/providers/[id]/route.ts +++ b/src/app/api/providers/[id]/route.ts @@ -207,6 +207,57 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id: updateData.providerSpecificData = normalizeProviderSpecificData(existing.provider, mergedPsd) || {}; + const psd = updateData.providerSpecificData as Record; + if (psd.apiKeyHealth) { + const health = psd.apiKeyHealth as Record; + + // If the primary API key was explicitly replaced in this request, + // clear stale health.primary — it no longer corresponds to the + // current key. The next health check will regenerate it. + if (updateData.apiKey !== undefined && updateData.apiKey !== existing.apiKey) { + delete health.primary; + } + + // Stale primary guard: no valid primary key → no primary health. + const currentApiKey = updateData.apiKey ?? existing.apiKey ?? null; + if (typeof currentApiKey !== "string" || currentApiKey.length === 0) { + delete health.primary; + } + + // Detect whether the extras list was explicitly changed by the caller. + // The index-based mapping (extra_0, extra_1, …) drifts when a key is + // inserted or removed mid-list, so we clear ALL extra health entries + // when the list actually changes and let the next health check regen. + const existingExtras = existingPsd.extraApiKeys; + const incomingExtras = incomingPsd?.extraApiKeys; + const extrasChanged = + Array.isArray(incomingExtras) && + (!Array.isArray(existingExtras) || + existingExtras.length !== incomingExtras.length || + existingExtras.some((v: string, i: number) => v !== incomingExtras[i])); + + const extras = psd.extraApiKeys; + const maxExtraIdx = Array.isArray(extras) ? extras.length : 0; + for (const key of Object.keys(health)) { + if (key.startsWith("extra_")) { + if (extrasChanged) { + // Extras modified — index drift possible. Clear all to be safe. + delete health[key]; + } else { + // Extras unchanged: only clean out-of-range indices. + const idx = parseInt(key.slice(6), 10); + if (isNaN(idx) || idx >= maxExtraIdx) { + delete health[key]; + } + } + } + } + + if (Object.keys(health).length === 0) { + delete psd.apiKeyHealth; + } + } + if (!isClaudeExtraUsageBlockEnabled(existing.provider, updateData.providerSpecificData)) { const clearExtraUsageUpdate = buildClaudeExtraUsageStateClearUpdate({ provider: existing.provider, diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index b82cd00532..cef3846634 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -22,6 +22,7 @@ import { resolveGitLabOAuthBaseUrl, } from "@/lib/oauth/gitlab"; import { providerAllowsOptionalApiKey } from "@/shared/constants/providers"; +import { removeConnectionHealth } from "@omniroute/open-sse/services/apiKeyRotator.ts"; // OAuth provider test endpoints const OAUTH_TEST_CONFIG = { @@ -676,6 +677,16 @@ export async function testSingleConnection(connectionId: string, validationModel if (result.valid) { updateData.backoffLevel = 0; + + const psd = connection?.providerSpecificData as Record | undefined; + updateData.providerSpecificData = { + ...(psd || {}), + apiKeyHealth: {}, + }; + + try { + removeConnectionHealth(connectionId); + } catch {} } // If token was refreshed, update tokens in DB diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 172aa56e52..1fa751ae8f 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index d6cad80969..f5f1bd5ecf 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3099,6 +3100,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "Google Cloud Project ID", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index ff0a0ee611..c86da33536 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 56fedca75d..6830e9582c 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 8fc4fa8479..8867358b51 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 0aff18fa7a..63d9954b5d 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index f127ea463f..4e5a9729d1 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index c63c430491..12515b7554 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3071,6 +3071,7 @@ "connected": "{count} Connected", "errorCount": "{count} Error ({code})", "errorCountNoCode": "{count} Error", + "warningCount": "{count} Warning", "noConnections": "No connections", "expiredBadge": "Expired", "expiringSoonBadge": "Expiring Soon", @@ -3589,6 +3590,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "Google Cloud Project ID", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index c6ce66b182..62001c7103 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 575800d1c1..11ef0e59b2 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index eec5c24014..0d9c2d0d95 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 765f558878..91a82a91ff 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index d26409a4b3..05dc724ef0 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 22177d1a70..39027436e0 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 3348ea0bb8..5e64d7f614 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 6fde38dab5..5acde4d1d2 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 391411755e..6981d1483c 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index f0aa839cc7..bf33646cda 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 42935d7ddd..264400a3f6 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index f7786d7fff..2720d73366 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 4c81677198..68a3e0baf0 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 74875b15fa..2f898add11 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 9c167f5c51..bf0d5d1ed6 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index e4b19ebbca..ca3f714e72 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index e5a9da6503..07d5aa5de1 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index ee3e199fee..f1b225df29 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 6491bd6c2a..27dda5c9bb 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index f4007a311e..289b2d22b9 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3195,6 +3196,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "O Google Programmable Search exige dois valores: sua chave de API e o Search Engine ID (cx) do painel do Programmable Search Engine.", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 051ba9aade..3abb567be9 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3193,6 +3194,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index e87f3ab168..73e6f0baa7 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 9f673cda5c..8160061634 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 4d5b936280..376e521287 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 837337f5ad..9fb4f18309 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index f0aa839cc7..bf33646cda 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index ef0629942e..c379916d11 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index b2c1ad237d..bb2f4c18f0 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 39a72a0079..c018017aad 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 4a016f5a99..c48c75a95d 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 1114ec05c0..8d7d1ddda9 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index b93e1ead75..ee2665f658 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 823027122c..b3f5485c91 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "Compatible Base Url Hint", "usageTracking": "Usage Tracking", "disableCloudTitle": "Disable Cloud Title", + "warningCount": "{count} Warning", "noConnections": "No Connections", "providerHealth": "Provider Health", "confirmDbImport": "Confirm Db Import", @@ -3196,6 +3197,8 @@ "primaryKey": "Primary Key", "apiKeyInvalidAlert": "{count} API key(s) marked as invalid due to authentication failures in connections: {connections}. They will be skipped in rotation. Click to review.", "apiKeyInvalidAlertTitle": "API Key Health Alert", + "apiKeyWarningAlert": "{count} API key(s) in warning state due to elevated failure rate in connections: {connections}. Review to prevent rotation issues.", + "apiKeyWarningAlertTitle": "API Key Warning", "googlePseInfo": "Google Pse Info", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 626989d9bb..697db12c09 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -181,6 +181,7 @@ "compatibleBaseUrlHint": "填写兼容 API 的 Base URL。", "usageTracking": "使用量跟踪", "disableCloudTitle": "禁用云端", + "warningCount": "{count} Warning", "noConnections": "无连接", "providerHealth": "提供商健康状态", "confirmDbImport": "确认DB导入", @@ -1065,7 +1066,8 @@ "updateNow": "立即更新", "updating": "更新中...", "updateAvailableDesc": "有新版本可用。点击更新。", - "updateStarted": "更新已开始..." + "updateStarted": "更新已开始...", + "providerTopology": "提供商拓扑" }, "analytics": { "title": "分析", @@ -3211,6 +3213,8 @@ "primaryKey": "主密钥", "apiKeyInvalidAlert": "{count} 个 API 密钥因认证失败被标记为无效:{connections}。轮换时将跳过它们。点击查看。", "apiKeyInvalidAlertTitle": "API 密钥健康提醒", + "apiKeyWarningAlert": "{count} 个 API 密钥在以下连接中处于警告状态:{connections}。请检查以防止轮换问题。", + "apiKeyWarningAlertTitle": "API 密钥警告", "googlePseInfo": "配置 Google Programmable Search Engine 以启用 Web Search。", "geminiCliProjectIdHint": "__MISSING__:Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.", "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", @@ -3301,7 +3305,17 @@ "freeTierProviders": "免费层提供商", "freeTierLabel": "有免费额度", "freeTierProvidersDesc": "提供免费层的提供商——有些需要注册 API 密钥,有些无需任何凭证。", - "showFreeOnly": "仅显示免费" + "showFreeOnly": "仅显示免费", + "ideProviders": "IDE 提供商", + "ideProvidersDesc": "内置 AI 订阅的编辑器。使用提供商页面直接从 IDE 密钥链导入凭据。", + "providerDetailFastTierTooltip": "默认对所有 Codex 连接应用 Codex Fast 层级", + "providerDetailFastDefaultLabel": "快速默认", + "providerDetailBrowserManualConnect": "浏览器/手动连接", + "providerDetailAuthUrl": "认证 URL", + "providerDetailCallbackUrl": "回调 URL", + "providerDetailValidClaudeCredentialsFile": "有效的 Claude 凭据文件", + "providerDetailPathAutoDetectedAllOs": "路径按操作系统自动检测(Linux/Mac/Windows)。", + "noIdeProviders": "没有符合当前筛选条件的 IDE 提供商。" }, "settings": { "title": "设置", diff --git a/src/shared/components/NotificationToast.tsx b/src/shared/components/NotificationToast.tsx index b5aee512f4..b7b6af861a 100644 --- a/src/shared/components/NotificationToast.tsx +++ b/src/shared/components/NotificationToast.tsx @@ -19,25 +19,27 @@ const ICONS = { info: "ℹ", }; +const BG_DARK = "rgba(30, 30, 30, 0.95)"; + const COLORS = { success: { - bg: "rgba(16, 185, 129, 0.15)", - border: "rgba(16, 185, 129, 0.4)", + bg: BG_DARK, + border: "rgba(16, 185, 129, 0.6)", icon: "#10b981", }, error: { - bg: "rgba(239, 68, 68, 0.15)", - border: "rgba(239, 68, 68, 0.4)", + bg: BG_DARK, + border: "rgba(239, 68, 68, 0.6)", icon: "#ef4444", }, warning: { - bg: "rgba(40, 28, 0, 0.92)", - border: "rgba(245, 158, 11, 0.7)", + bg: BG_DARK, + border: "rgba(245, 158, 11, 0.6)", icon: "#fbbf24", }, info: { - bg: "rgba(59, 130, 246, 0.15)", - border: "rgba(59, 130, 246, 0.4)", + bg: BG_DARK, + border: "rgba(59, 130, 246, 0.6)", icon: "#3b82f6", }, }; @@ -114,7 +116,10 @@ function Toast({ notification, onDismiss }) {
{notification.dismissible && (