From 75d0d7299acf07752eb6b4a88cb188c078a69836 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 03:59:06 -0300 Subject: [PATCH 001/112] fix(translator): inject web_search tool in Responses-API flat shape (#2390) The omniroute_web_search fallback tool was always built in Chat Completions nested shape ({type, function:{name}}). On the Responses->Responses passthrough path nothing flattens it, so Codex/relay upstreams rejected it with 'Missing required parameter: tools[0].name'. buildFallbackTool and the tool_choice injection now emit the flat Responses-API shape ({type, name}) when the target provider speaks the Responses API. --- open-sse/services/webSearchFallback.ts | 37 +++++++--- tests/unit/web-search-fallback-format.test.ts | 74 +++++++++++++++++++ 2 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 tests/unit/web-search-fallback-format.test.ts diff --git a/open-sse/services/webSearchFallback.ts b/open-sse/services/webSearchFallback.ts index 89b18deb00..62f3f06ca5 100644 --- a/open-sse/services/webSearchFallback.ts +++ b/open-sse/services/webSearchFallback.ts @@ -109,14 +109,23 @@ function buildFallbackParameters(tool: JsonRecord): JsonRecord { }; } -function buildFallbackTool(tool: JsonRecord): JsonRecord { +function buildFallbackTool(tool: JsonRecord, targetFormat?: string | null): JsonRecord { + const name = OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME; + const description = buildFallbackDescription(tool); + const parameters = buildFallbackParameters(tool); + + // Responses API expects FLAT function tools ({ type, name, parameters }), whereas + // Chat Completions expects NESTED ({ type, function: { name, parameters } }). On the + // Responses→Responses passthrough path nothing flattens the injected tool, so a nested + // shape reaches the upstream as `tools[0].function.name` and is rejected with + // "Missing required parameter: 'tools[0].name'." (issue #2390). + if (targetFormat === FORMATS.OPENAI_RESPONSES) { + return { type: "function", name, description, parameters }; + } + return { type: "function", - function: { - name: OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME, - description: buildFallbackDescription(tool), - parameters: buildFallbackParameters(tool), - }, + function: { name, description, parameters }, }; } @@ -182,8 +191,12 @@ export function prepareWebSearchFallbackBody( return true; }); + const isResponsesTarget = options.targetFormat === FORMATS.OPENAI_RESPONSES; + if (!toolNames.has(OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME)) { - preservedTools.unshift(buildFallbackTool(toRecord(builtInSearchTools[0]))); + preservedTools.unshift( + buildFallbackTool(toRecord(builtInSearchTools[0]), options.targetFormat) + ); } const nextBody: T = { @@ -192,10 +205,12 @@ export function prepareWebSearchFallbackBody( }; if (isBuiltInWebSearchToolChoice(body.tool_choice)) { - nextBody.tool_choice = { - type: "function", - function: { name: OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME }, - } as T["tool_choice"]; + // Match the injected tool shape: flat for Responses API, nested for Chat Completions. + nextBody.tool_choice = ( + isResponsesTarget + ? { type: "function", name: OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } + : { type: "function", function: { name: OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } } + ) as T["tool_choice"]; } return { diff --git a/tests/unit/web-search-fallback-format.test.ts b/tests/unit/web-search-fallback-format.test.ts new file mode 100644 index 0000000000..605aa1f7dd --- /dev/null +++ b/tests/unit/web-search-fallback-format.test.ts @@ -0,0 +1,74 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { prepareWebSearchFallbackBody, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } = + await import("../../open-sse/services/webSearchFallback.ts"); + +// Regression for #2390: when the target is a Responses-API provider, the injected +// omniroute_web_search tool must use the FLAT function shape ({ type, name }), not the +// nested Chat Completions shape ({ type, function: { name } }). On the Responses→Responses +// passthrough path nothing flattens it, so a nested tool reaches the upstream as +// tools[0].function.name and is rejected with "Missing required parameter: 'tools[0].name'". + +function makeBody() { + return { + model: "gpt-5.5", + messages: [{ role: "user", content: "search the web" }], + tools: [{ type: "web_search" }], + }; +} + +test("#2390 web_search fallback is FLAT for Responses API target", () => { + const { body, fallback } = prepareWebSearchFallbackBody(makeBody(), { + targetFormat: "openai-responses", + nativeCodexPassthrough: false, + }); + + assert.equal(fallback.enabled, true); + const injected = body.tools[0] as Record; + assert.equal(injected.type, "function"); + assert.equal(injected.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); + assert.equal( + injected.function, + undefined, + "Responses API tool must not be nested under .function" + ); + assert.ok(injected.parameters, "flat tool keeps top-level parameters"); +}); + +test("#2390 web_search fallback stays NESTED for Chat Completions target", () => { + const { body, fallback } = prepareWebSearchFallbackBody(makeBody(), { + targetFormat: "openai", + nativeCodexPassthrough: false, + }); + + assert.equal(fallback.enabled, true); + const injected = body.tools[0] as Record; + assert.equal(injected.type, "function"); + const fn = injected.function as Record | undefined; + assert.ok(fn, "Chat Completions tool must be nested under .function"); + assert.equal(fn?.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); + assert.equal( + injected.name, + undefined, + "Chat Completions tool must not expose a flat top-level name" + ); +}); + +test("#2390 tool_choice matches the injected tool shape per target format", () => { + const responses = prepareWebSearchFallbackBody( + { ...makeBody(), tool_choice: { type: "web_search" } }, + { targetFormat: "openai-responses", nativeCodexPassthrough: false } + ); + const rChoice = responses.body.tool_choice as Record; + assert.equal(rChoice.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); + assert.equal(rChoice.function, undefined); + + const chat = prepareWebSearchFallbackBody( + { ...makeBody(), tool_choice: { type: "web_search" } }, + { targetFormat: "openai", nativeCodexPassthrough: false } + ); + const cChoice = chat.body.tool_choice as Record; + const cFn = cChoice.function as Record | undefined; + assert.equal(cFn?.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); +}); From 016deae964a5b914c86fe457f339a87003e13ad8 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 04:00:18 -0300 Subject: [PATCH 002/112] fix(kiro): serialize non-string role:tool content for CodeWhisperer (#2446) An OpenAI-style role:"tool" message carrying structured/array content was collapsing to content:[{ text: "" }], which CodeWhisperer rejects with 400 'Improperly formed request'. Reuse serializeToolResultContent (already used by the Anthropic tool_result path) so structured output is never empty. --- open-sse/translator/request/openai-to-kiro.ts | 6 ++- tests/unit/translator-openai-to-kiro.test.ts | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index 0492fdff66..9fb12bcb20 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -284,7 +284,11 @@ function convertMessages(messages, tools, model) { // Handle tool role (from normalized) if (msg.role === "tool") { - const toolContent = typeof msg.content === "string" ? msg.content : ""; + // Reuse the shared serializer so non-string content (arrays, structured/JSON + // blocks, images) is never collapsed to an empty string. CodeWhisperer rejects a + // toolResult whose content is [{ text: "" }] with 400 "Improperly formed request" + // — the same failure mode that hit the Anthropic tool_result path (issue #2446). + const toolContent = serializeToolResultContent(msg.content); pendingToolResults.push({ toolUseId: msg.tool_call_id, status: "success", diff --git a/tests/unit/translator-openai-to-kiro.test.ts b/tests/unit/translator-openai-to-kiro.test.ts index d14d3e3c29..4ed7067f3c 100644 --- a/tests/unit/translator-openai-to-kiro.test.ts +++ b/tests/unit/translator-openai-to-kiro.test.ts @@ -866,3 +866,52 @@ test("OpenAI -> Kiro generates stable non-random toolUseId when tool_call has no assert.ok(id1, "toolUseId must be set even when id is absent"); assert.equal(id1, id2, "toolUseId must be deterministic (same input → same id)"); }); + +// Regression for #2446: an OpenAI-style `role:"tool"` message carrying NON-string +// (structured / array) content must not collapse to `content:[{ text: "" }]` — +// CodeWhisperer rejects an empty toolResult with 400 "Improperly formed request". +test("OpenAI -> Kiro serializes non-string role:tool content to non-empty text (#2446)", () => { + const result = buildKiroPayload( + "claude-sonnet-4", + { + messages: [ + { role: "user", content: "list the files" }, + { + role: "assistant", + tool_calls: [ + { + id: "call_mem", + type: "function", + function: { name: "read_memory", arguments: "{}" }, + }, + ], + }, + { + role: "tool", + tool_call_id: "call_mem", + content: [ + { type: "text", text: "entry A" }, + { type: "text", text: "entry B" }, + ], + }, + { role: "user", content: "thanks" }, + ], + }, + true, + null + ); + + const cs = result.conversationState as any; + const contexts = [ + cs.currentMessage?.userInputMessage?.userInputMessageContext, + ...(cs.history as any[]).map((h) => h.userInputMessage?.userInputMessageContext), + ]; + const toolResults = contexts + .map((c) => c?.toolResults) + .find((tr) => Array.isArray(tr) && tr.some((r: any) => r.toolUseId === "call_mem")); + assert.ok(toolResults, "tool role must produce a toolResult"); + const result0 = toolResults.find((r: any) => r.toolUseId === "call_mem"); + const text = result0.content[0].text as string; + assert.notEqual(text, "", "non-string tool content must not collapse to empty string"); + assert.match(text, /entry A/, "serialized content preserves the structured text blocks"); +}); From 6fe366db5458e791127938e440fca92fdf854c06 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 04:01:11 -0300 Subject: [PATCH 003/112] fix(claude): per-model beta gating + passthrough thinking sanitization (#2454) selectBetaFlags now gates the heavy-agent betas (context-1m, effort, advanced-tool-use) on Opus/Sonnet only; Haiku with OAuth was rejecting context-1m with 400 'incompatible with the long context beta header'. base.ts stops deleting Haiku's thinking config (real Claude Desktop keeps it). chatCore passthrough converts historical thinking/redacted_thinking blocks to redacted_thinking with a synthetic signature, fixing 400 'Invalid signature in thinking block' on mid-session model switches. Co-authored analysis by havockdev. --- open-sse/executors/base.ts | 4 +- open-sse/executors/claudeIdentity.ts | 39 ++++++++-- open-sse/handlers/chatCore.ts | 42 +++++++++++ tests/unit/claude-beta-flags-2454.test.ts | 50 +++++++++++++ .../claude-passthrough-thinking-2454.test.ts | 73 +++++++++++++++++++ 5 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 tests/unit/claude-beta-flags-2454.test.ts create mode 100644 tests/unit/claude-passthrough-thinking-2454.test.ts diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 6fa9f258d8..392ddc1a7b 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -715,7 +715,9 @@ export class BaseExecutor { // Default CC logic when no override headers are present const isHaiku = typeof tb.model === "string" && tb.model.includes("haiku"); if (isHaiku) { - delete tb.thinking; + // Keep tb.thinking — real Claude Desktop keeps thinking enabled for Haiku + // (issue #2454). Only strip output_config (effort) which Haiku rejects; + // context_management is re-paired with the preserved thinking below. delete tb.output_config; delete tb.context_management; } else if (tb.thinking === undefined && tb.output_config === undefined) { diff --git a/open-sse/executors/claudeIdentity.ts b/open-sse/executors/claudeIdentity.ts index 30e081acd9..4b4ea9bf5d 100644 --- a/open-sse/executors/claudeIdentity.ts +++ b/open-sse/executors/claudeIdentity.ts @@ -284,12 +284,34 @@ export function parseUpstreamMetadataUserId( // ---------- anthropic-beta selector -------------------------------------- +/** + * Models that support the heavy-agent beta tier (context-1m, effort, + * advanced-tool-use). Only Opus/Sonnet are eligible — Haiku with OAuth + * authentication rejects `context-1m-2025-08-07` with + * "This authentication style is incompatible with the long context beta header" + * (issue #2454). Matches real Claude Desktop, which omits these flags for Haiku. + */ +const HEAVY_AGENT_BETA_MODEL_PREFIXES = ["claude-opus", "claude-sonnet"]; + +function isHeavyAgentModel(model: unknown): boolean { + if (typeof model !== "string") return false; + const normalized = model.toLowerCase(); + return HEAVY_AGENT_BETA_MODEL_PREFIXES.some((prefix) => normalized.includes(prefix)); +} + /** * Pick the anthropic-beta flag set that matches the request shape. Real CLI * uses three patterns: minimal probe, structured-output, and full agent. * Sending the full set on every shape is itself a fingerprint. + * + * The heavy-agent flags (context-1m, effort, advanced-tool-use) are gated on + * the model as well as the shape: Haiku must never receive `context-1m`, which + * Anthropic rejects under OAuth authentication. */ -export function selectBetaFlags(body: Record | null | undefined): string { +export function selectBetaFlags( + body: Record | null | undefined, + model?: string | null +): string { const b = body || {}; const hasSystem = !!b.system && @@ -301,11 +323,13 @@ export function selectBetaFlags(body: Record | null | undefined !!(outputCfg && (outputCfg.format as { type?: string } | undefined)?.type === "json_schema") || !!(b.response_format as { type?: string } | undefined)?.type; const isFullAgent = hasTools && hasSystem; + const effectiveModel = model ?? (typeof b.model === "string" ? b.model : ""); + const isHeavyAgent = isFullAgent && isHeavyAgentModel(effectiveModel); const flags: string[] = []; if (isFullAgent) flags.push("claude-code-20250219"); flags.push("oauth-2025-04-20"); - if (isFullAgent) flags.push("context-1m-2025-08-07"); + if (isHeavyAgent) flags.push("context-1m-2025-08-07"); flags.push( "interleaved-thinking-2025-05-14", "redact-thinking-2026-02-12", @@ -314,12 +338,11 @@ export function selectBetaFlags(body: Record | null | undefined ); if (hasStructuredOutput || isFullAgent) flags.push("advisor-tool-2026-03-01"); if (hasStructuredOutput && !isFullAgent) flags.push("structured-outputs-2025-12-15"); - if (isFullAgent) { - flags.push( - "advanced-tool-use-2025-11-20", - "effort-2025-11-24", - "extended-cache-ttl-2025-04-11" - ); + // extended-cache-ttl is sent for all full-agent shapes (incl. Haiku); the + // heavier advanced-tool-use / effort flags are Opus/Sonnet-only. + if (isFullAgent) flags.push("extended-cache-ttl-2025-04-11"); + if (isHeavyAgent) { + flags.push("advanced-tool-use-2025-11-20", "effort-2025-11-24"); } return flags.join(","); } diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 74a059e2e3..d6e59665b5 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -17,6 +17,7 @@ import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/ import { refreshWithRetry, isUnrecoverableRefreshError } from "../services/tokenRefresh.ts"; import { createRequestLogger } from "../utils/requestLogger.ts"; import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts"; +import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../config/defaultThinkingSignature.ts"; import { getStripTypesForProviderModel, stripIncompatibleMessageContent, @@ -434,6 +435,36 @@ export function shouldUseNativeCodexPassthrough({ return segments.includes("responses"); } +/** + * Convert all historical `thinking` / `redacted_thinking` blocks in assistant + * messages to `redacted_thinking` carrying a synthetic default signature. + * + * A thinking block's `signature` is cryptographically bound to the auth token + * that generated it. In Anthropic-native Claude OAuth passthrough, when a session + * starts on one model (token A) and then switches model or falls over (token B), + * Anthropic rejects every historical signature with 400 "Invalid signature in + * thinking block" (issue #2454). `redacted_thinking` bypasses signature validation. + * + * ALL assistant turns are converted, including the last — under a different token + * every signature is invalid, so there is no "preserve latest" exception. Returns a + * new messages array (original is not mutated) only touching messages that changed. + */ +export function redactPassthroughThinkingSignatures(messages: unknown, signature: string): unknown { + if (!Array.isArray(messages)) return messages; + return (messages as Record[]).map((msg) => { + if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) return msg; + let modified = false; + const newContent = (msg.content as Record[]).map((block) => { + if (block && (block.type === "thinking" || block.type === "redacted_thinking")) { + modified = true; + return { type: "redacted_thinking", data: signature }; + } + return block; + }); + return modified ? { ...msg, content: newContent } : msg; + }); +} + export function isClaudeCodeSemanticPassthroughRequest({ provider, sourceFormat, @@ -2742,6 +2773,17 @@ export async function handleChatCore({ // regardless of combo strategy or cache_control settings. translatedBody = { ...body }; translatedBody._disableToolPrefix = true; + + // Sanitize historical thinking-block signatures for Anthropic-native Claude OAuth. + // Only Anthropic's first-party API validates these signatures (token-bound); third-party + // Claude-shape providers do not. See redactPassthroughThinkingSignatures + issue #2454. + if (provider === "claude") { + translatedBody.messages = redactPassthroughThinkingSignatures( + translatedBody.messages, + DEFAULT_THINKING_CLAUDE_SIGNATURE + ) as typeof translatedBody.messages; + } + if (!isClaudeCodeSemanticPassthrough) { normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true }); } else { diff --git a/tests/unit/claude-beta-flags-2454.test.ts b/tests/unit/claude-beta-flags-2454.test.ts new file mode 100644 index 0000000000..3dbd3e4e9d --- /dev/null +++ b/tests/unit/claude-beta-flags-2454.test.ts @@ -0,0 +1,50 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { selectBetaFlags } = await import("../../open-sse/executors/claudeIdentity.ts"); + +// Regression for #2454: Haiku with OAuth rejects `context-1m-2025-08-07` with +// 400 "This authentication style is incompatible with the long context beta header". +// The heavy-agent beta tier (context-1m, effort, advanced-tool-use) must be gated on +// Opus/Sonnet only; Haiku still receives the general full-agent flags. + +function fullAgentBody(model: string) { + return { + model, + system: "You are a coding agent.", + tools: [{ name: "read_file", description: "x", input_schema: { type: "object" } }], + }; +} + +test("#2454 Haiku full-agent omits heavy-agent beta flags", () => { + const flags = selectBetaFlags(fullAgentBody("claude-haiku-4-5-20251001")); + assert.ok(!flags.includes("context-1m-2025-08-07"), "Haiku must NOT receive context-1m"); + assert.ok(!flags.includes("effort-2025-11-24"), "Haiku must NOT receive effort"); + assert.ok( + !flags.includes("advanced-tool-use-2025-11-20"), + "Haiku must NOT receive advanced-tool-use" + ); + // General full-agent flags are still present for Haiku. + assert.ok(flags.includes("oauth-2025-04-20")); + assert.ok(flags.includes("interleaved-thinking-2025-05-14")); + assert.ok(flags.includes("claude-code-20250219")); + assert.ok(flags.includes("extended-cache-ttl-2025-04-11")); +}); + +test("#2454 Sonnet full-agent includes heavy-agent beta flags", () => { + const flags = selectBetaFlags(fullAgentBody("claude-sonnet-4-6")); + assert.ok(flags.includes("context-1m-2025-08-07"), "Sonnet should receive context-1m"); + assert.ok(flags.includes("effort-2025-11-24")); + assert.ok(flags.includes("advanced-tool-use-2025-11-20")); +}); + +test("#2454 Opus full-agent includes heavy-agent beta flags", () => { + const flags = selectBetaFlags(fullAgentBody("claude-opus-4-7")); + assert.ok(flags.includes("context-1m-2025-08-07"), "Opus should receive context-1m"); +}); + +test("#2454 explicit model arg overrides body.model for tiering", () => { + // body says sonnet, but the resolved upstream model is haiku → must omit context-1m + const flags = selectBetaFlags(fullAgentBody("claude-sonnet-4-6"), "claude-haiku-4-5-20251001"); + assert.ok(!flags.includes("context-1m-2025-08-07")); +}); diff --git a/tests/unit/claude-passthrough-thinking-2454.test.ts b/tests/unit/claude-passthrough-thinking-2454.test.ts new file mode 100644 index 0000000000..7878ce7e68 --- /dev/null +++ b/tests/unit/claude-passthrough-thinking-2454.test.ts @@ -0,0 +1,73 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { redactPassthroughThinkingSignatures } = await import("../../open-sse/handlers/chatCore.ts"); + +const SIG = "SYNTHETIC_SIGNATURE_FIXTURE"; + +// Regression for #2454 (Error 2): historical thinking signatures are bound to the +// original auth token; after a model switch the proxy uses a different token and +// Anthropic rejects them. Convert ALL thinking/redacted_thinking blocks (incl. last). + +test("#2454 converts all thinking blocks to redacted_thinking with synthetic signature", () => { + const messages = [ + { role: "user", content: [{ type: "text", text: "hi" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "reasoning", signature: "TOKEN_A_SIG_1" }, + { type: "text", text: "answer 1" }, + ], + }, + { role: "user", content: [{ type: "text", text: "more" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "reasoning 2", signature: "TOKEN_A_SIG_2" }, + { type: "text", text: "answer 2" }, + ], + }, + ]; + + const out = redactPassthroughThinkingSignatures(messages, SIG) as any[]; + + for (const msg of out) { + if (msg.role !== "assistant") continue; + for (const block of msg.content) { + if (block.type === "redacted_thinking") { + assert.equal(block.data, SIG, "redacted_thinking carries the synthetic signature"); + assert.equal(block.signature, undefined, "original signature is dropped"); + assert.equal(block.thinking, undefined, "plaintext thinking is dropped"); + } + assert.notEqual(block.type, "thinking", "no raw thinking block must survive (incl. last)"); + } + } + // text blocks are preserved verbatim + assert.equal(out[1].content[1].text, "answer 1"); + assert.equal(out[3].content[1].text, "answer 2"); +}); + +test("#2454 leaves messages without thinking untouched (same reference)", () => { + const messages = [ + { role: "user", content: [{ type: "text", text: "hi" }] }, + { role: "assistant", content: [{ type: "text", text: "plain answer" }] }, + ]; + const out = redactPassthroughThinkingSignatures(messages, SIG) as any[]; + assert.equal(out[1], messages[1], "unchanged assistant message keeps its reference"); +}); + +test("#2454 already-redacted thinking blocks are re-stamped with the synthetic signature", () => { + const messages = [ + { + role: "assistant", + content: [{ type: "redacted_thinking", data: "STALE_TOKEN_B_DATA" }], + }, + ]; + const out = redactPassthroughThinkingSignatures(messages, SIG) as any[]; + assert.equal(out[0].content[0].data, SIG); +}); + +test("#2454 non-array messages pass through", () => { + assert.equal(redactPassthroughThinkingSignatures(undefined, SIG), undefined); + assert.equal(redactPassthroughThinkingSignatures(null, SIG), null); +}); From 6db9a77513bca20936ece96d9b02a5b11fc2ffd7 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 04:02:04 -0300 Subject: [PATCH 004/112] fix(perplexity-web): TLS impersonation to bypass Cloudflare on VPS (#2459) New perplexityTlsClient.ts (Firefox-148 TLS profile, mirrors chatgptTlsClient) routes perplexity-web requests so Cloudflare stops 403-challenging datacenter IPs. Executor and connection validator now distinguish a Cloudflare block from an invalid session cookie. Adds OMNIROUTE_PPLX_TLS_TIMEOUT_MS / OMNIROUTE_PPLX_TLS_GRACE_MS. Co-authored analysis by havockdev. --- .env.example | 7 + docs/reference/ENVIRONMENT.md | 2 + open-sse/executors/perplexity-web.ts | 52 +- open-sse/services/perplexityTlsClient.ts | 597 +++++++++++++++++++++++ src/lib/providers/validation.ts | 79 ++- tests/unit/perplexity-tls-client.test.ts | 42 ++ tests/unit/perplexity-web.test.ts | 78 ++- 7 files changed, 812 insertions(+), 45 deletions(-) create mode 100644 open-sse/services/perplexityTlsClient.ts create mode 100644 tests/unit/perplexity-tls-client.test.ts diff --git a/.env.example b/.env.example index 1bda4388c7..a616c2cab2 100644 --- a/.env.example +++ b/.env.example @@ -706,6 +706,13 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS=60000 # OMNIROUTE_CLAUDE_TLS_GRACE_MS=10000 +# ── Perplexity TLS sidecar (Firefox-fingerprinted client) ── +# Used by: open-sse/services/perplexityTlsClient.ts — wire-level timeout for +# the bogdanfinn/tls-client koffi binding and the JS-side grace window +# layered on top of it when the native library is wedged. +# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000 +# OMNIROUTE_PPLX_TLS_GRACE_MS=10000 + # ── Circuit breaker thresholds and reset windows ── # Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts. # Defaults match historical PROVIDER_PROFILES values (post-scaling for diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 17458638e6..a322ccb7cb 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -528,6 +528,8 @@ REQUEST_TIMEOUT_MS (global override) | `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | | `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`claudeTlsClient.ts`). | | `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | +| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`perplexityTlsClient.ts`). | +| `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | ### Circuit Breaker Thresholds diff --git a/open-sse/executors/perplexity-web.ts b/open-sse/executors/perplexity-web.ts index 3695f734aa..3d74340cb8 100644 --- a/open-sse/executors/perplexity-web.ts +++ b/open-sse/executors/perplexity-web.ts @@ -7,11 +7,19 @@ */ import { BaseExecutor, type ExecuteInput } from "./base.ts"; +import { + tlsFetchPerplexity, + isCloudflareChallenge, + TlsClientUnavailableError, + type TlsFetchResult, +} from "../services/perplexityTlsClient.ts"; const PPLX_SSE_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask"; const PPLX_API_VERSION = "client-1.11.0"; +// Firefox 148 — must match the `firefox_148` TLS profile used by perplexityTlsClient. +// A mismatched UA vs TLS fingerprint is itself a Cloudflare bot signal (issue #2459). const PPLX_USER_AGENT = - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"; + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0"; const MODEL_MAP: Record = { "pplx-auto": ["concise", "pplx_pro"], @@ -721,23 +729,29 @@ export class PerplexityWebExecutor extends BaseExecutor { `Query to ${model} (pref=${modelPref}, mode=${pplxMode}), len=${query.length}` ); - // Fetch from Perplexity - const fetchOptions: RequestInit = { - method: "POST", - headers, - body: JSON.stringify(pplxBody), - }; - if (signal) fetchOptions.signal = signal; - - let response: Response; + // Fetch from Perplexity through the Firefox-fingerprinted TLS client. + // Perplexity sits behind Cloudflare Enterprise which pins JA3/JA4 to a real + // browser handshake; Node's fetch() is challenged with a 403 page from + // VPS/datacenter IPs even with a valid cookie (issue #2459). + let response: TlsFetchResult; try { - response = await fetch(PPLX_SSE_ENDPOINT, fetchOptions); + response = await tlsFetchPerplexity(PPLX_SSE_ENDPOINT, { + method: "POST", + headers, + body: JSON.stringify(pplxBody), + signal: signal ?? null, + stream: true, + streamEofSymbol: "[DONE]", + }); } catch (err) { + const isTlsUnavail = err instanceof TlsClientUnavailableError; log?.error?.("PPLX-WEB", `Fetch failed: ${err instanceof Error ? err.message : String(err)}`); const errResp = new Response( JSON.stringify({ error: { - message: `Perplexity connection failed: ${err instanceof Error ? err.message : String(err)}`, + message: isTlsUnavail + ? `Perplexity TLS client unavailable: ${(err as Error).message}` + : `Perplexity connection failed: ${err instanceof Error ? err.message : String(err)}`, type: "upstream_error", }, }), @@ -746,12 +760,20 @@ export class PerplexityWebExecutor extends BaseExecutor { return { response: errResp, url: PPLX_SSE_ENDPOINT, headers, transformedBody: pplxBody }; } - if (!response.ok) { + if (response.status !== 200 || (!response.body && !response.text)) { const status = response.status; let errMsg = `Perplexity returned HTTP ${status}`; if (status === 401 || status === 403) { - errMsg = - "Perplexity auth failed — session cookie may be expired. Re-paste your __Secure-next-auth.session-token."; + if (isCloudflareChallenge(response.text)) { + errMsg = + "Cloudflare blocked the request — Perplexity's edge rejected this server's TLS fingerprint " + + "(common on VPS/datacenter IPs). Ensure tls-client-node is installed with its native binary, " + + "or route perplexity-web through a residential proxy."; + log?.error?.("PPLX-WEB", "Cloudflare challenge detected — TLS bypass failed"); + } else { + errMsg = + "Perplexity auth failed — session cookie may be expired. Re-paste your __Secure-next-auth.session-token."; + } } else if (status === 429) { errMsg = "Perplexity rate limited. Wait a moment and retry."; } diff --git a/open-sse/services/perplexityTlsClient.ts b/open-sse/services/perplexityTlsClient.ts new file mode 100644 index 0000000000..debcd3e682 --- /dev/null +++ b/open-sse/services/perplexityTlsClient.ts @@ -0,0 +1,597 @@ +/** + * Browser-TLS-impersonating HTTP client for www.perplexity.ai. + * + * Why this exists: Perplexity sits behind the same Cloudflare Enterprise + * configuration as ChatGPT — it pins access to the client's TLS fingerprint + * (JA3/JA4) + HTTP/2 SETTINGS frame ordering. Node's Undici fetch presents an + * obvious "not a browser" handshake and gets challenged with a 403 "Just a + * moment..." page from VPS/datacenter IPs — even with a valid session cookie. + * This module wraps `tls-client-node` (native shared library built from + * bogdanfinn/tls-client) to send a Firefox handshake instead. (issue #2459) + * + * Mirrors `chatgptTlsClient.ts`; kept as an independent module so changes here + * cannot regress the production chatgpt-web path. The first call lazily starts + * the managed sidecar; subsequent calls reuse a singleton TLSClient. Process + * exit hooks stop the sidecar cleanly. + */ + +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { mkdtemp, open, unlink, rmdir, stat } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; + +let clientPromise: Promise | null = null; +let exitHookInstalled = false; + +const PPLX_PROFILE = "firefox_148"; // matches the Firefox 148 UA we send +const DEFAULT_TIMEOUT_MS = + Number.parseInt(process.env.OMNIROUTE_PPLX_TLS_TIMEOUT_MS || "", 10) || 30_000; +// Grace period added to the binding's wire-level timeout before our JS-level +// hard timeout fires. Under healthy operation `tls-client-node` honors +// `timeoutMilliseconds` and rejects on its own; the JS-level race only wins +// when the koffi-loaded native library is wedged (which the binding's own +// timer can't escape). Keep the grace small so users don't wait noticeably +// longer than the configured timeout when the binding is dead. +const HARD_TIMEOUT_GRACE_MS = + Number.parseInt(process.env.OMNIROUTE_PPLX_TLS_GRACE_MS || "", 10) || 10_000; + +function installExitHook(): void { + if (exitHookInstalled) return; + exitHookInstalled = true; + const stop = async () => { + if (!clientPromise) return; + try { + const c = (await clientPromise) as { stop?: () => Promise }; + await c.stop?.(); + } catch { + // ignore + } + }; + process.once("beforeExit", stop); + process.once("SIGINT", () => { + void stop(); + }); + process.once("SIGTERM", () => { + void stop(); + }); +} + +/** + * Drop the cached client so the next `getClient()` call respawns it. Called + * when a request observes the native binding has wedged — releasing the + * reference lets a fresh TLSClient (and a fresh koffi load) take over without + * a process restart. + */ +function resetClientCache(): void { + clientPromise = null; +} + +export class TlsClientHangError extends Error { + constructor(message: string) { + super(message); + this.name = "TlsClientHangError"; + } +} + +/** + * Race a `client.request()` promise against (a) a JS-level hard timeout and + * (b) the caller's abort signal. The native binding's `timeoutMilliseconds` + * already covers the wire path; this guards the case where the koffi binding + * itself deadlocks (observed after sustained load), where neither the + * binding's own timer nor a post-call `signal.aborted` re-check can recover. + */ +async function raceWithTimeout( + promise: Promise, + timeoutMs: number, + signal: AbortSignal | null | undefined +): Promise { + let timer: ReturnType | null = null; + let abortListener: (() => void) | null = null; + try { + const racers: Promise[] = [ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => { + reject( + new TlsClientHangError( + `tls-client-node call exceeded ${timeoutMs}ms — native binding likely deadlocked` + ) + ); + }, timeoutMs); + }), + ]; + if (signal) { + racers.push( + new Promise((_, reject) => { + if (signal.aborted) { + reject(makeAbortError(signal)); + return; + } + abortListener = () => reject(makeAbortError(signal)); + signal.addEventListener("abort", abortListener, { once: true }); + }) + ); + } + return await Promise.race(racers); + } finally { + if (timer) clearTimeout(timer); + if (signal && abortListener) signal.removeEventListener("abort", abortListener); + } +} + +async function getClient(): Promise<{ + request: (url: string, opts: Record) => Promise; +}> { + if (!clientPromise) { + clientPromise = (async () => { + try { + const mod = await import("tls-client-node"); + const TLSClient = (mod as { TLSClient: new (opts?: Record) => unknown }) + .TLSClient; + // Native mode loads the shared library directly via koffi, avoiding the + // managed sidecar's localhost HTTP calls that OmniRoute's global fetch + // proxy patch interferes with. + const client = new TLSClient({ runtimeMode: "native" }) as { + start: () => Promise; + request: (url: string, opts: Record) => Promise; + }; + await client.start(); + + installExitHook(); + return client; + } catch (err) { + clientPromise = null; + const msg = err instanceof Error ? err.message : String(err); + throw new TlsClientUnavailableError( + `TLS impersonation client failed to start: ${msg}. ` + + `Verify tls-client-node is installed and its native binary downloaded.` + ); + } + })(); + } + return clientPromise as Promise<{ + request: (url: string, opts: Record) => Promise; + }>; +} + +interface TlsResponseLike { + status: number; + headers: Record; + body: string; // for non-streaming requests, the full response body + cookies?: Record; + text: () => Promise; + bytes: () => Promise; + json: () => Promise; +} + +export class TlsClientUnavailableError extends Error { + constructor(message: string) { + super(message); + this.name = "TlsClientUnavailableError"; + } +} + +export interface TlsFetchOptions { + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + headers?: Record; + body?: string; + timeoutMs?: number; + signal?: AbortSignal | null; + /** + * If true, the response body is streamed to a temp file and exposed as a + * ReadableStream. Use for SSE responses (the perplexity_ask + * endpoint). Otherwise, the full body is read into memory. + */ + stream?: boolean; + /** EOF marker the upstream sends to signal end of stream (default: "[DONE]"). */ + streamEofSymbol?: string; + /** + * Optional upstream proxy URL (`http://user:pass@host:port` or + * `socks5://...`). When set, the request is tunneled through this proxy + * before reaching perplexity.ai. + * + * Resolution order: + * 1. `options.proxyUrl` (per-call override from caller) + * 2. `process.env.OMNIROUTE_TLS_PROXY_URL` (single-flag opt-in) + * 3. `process.env.HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` (POSIX-standard fallback) + * + * The native `tls-client-node` binding does **not** consult Go's + * `http.ProxyFromEnvironment`, so the env vars need to be plumbed in here at + * the JS layer. + */ + proxyUrl?: string; +} + +import { resolveProxyForRequest } from "../utils/proxyFetch.ts"; + +/** + * Resolve the proxy URL for a tls-client request. Per-call value wins; + * otherwise we use the standard proxy fetch resolution which reads from + * the dashboard AsyncLocalStorage context or falls back to env vars. + */ +function resolveProxyUrl(perCall: string | undefined): string | undefined { + if (perCall && perCall.length > 0) return perCall; + try { + const proxyInfo = resolveProxyForRequest("https://www.perplexity.ai"); + if (proxyInfo && proxyInfo.proxyUrl) { + return proxyInfo.proxyUrl; + } + } catch { + // Ignore resolution errors + } + return undefined; +} + +export interface TlsFetchResult { + status: number; + headers: Headers; + /** Full response body as text — only populated for non-streaming requests. */ + text: string | null; + /** Streaming body — only populated when options.stream === true. */ + body: ReadableStream | null; +} + +// Test-only injection point. Tests call __setTlsFetchOverrideForTesting() +// to replace the real TLS client with a mock; production never touches this. +let testOverride: ((url: string, options: TlsFetchOptions) => Promise) | null = + null; + +export function __setTlsFetchOverrideForTesting(fn: typeof testOverride): void { + testOverride = fn; +} + +/** + * Make a single HTTP request to perplexity.ai with a Firefox-like TLS fingerprint. + * + * Throws TlsClientUnavailableError if the native binary failed to load. + */ +export async function tlsFetchPerplexity( + url: string, + options: TlsFetchOptions = {} +): Promise { + if (testOverride) return testOverride(url, options); + // Honor abort signals up-front. tls-client-node's koffi binding doesn't + // accept an AbortSignal mid-flight (the binary call is opaque), so the best + // we can do is bail before issuing the call. We also re-check after — if + // the caller aborted while the upstream was running, throw rather than + // returning a stale response so the caller doesn't try to use it. + if (options.signal?.aborted) { + throw makeAbortError(options.signal); + } + const client = await getClient(); + if (options.signal?.aborted) { + throw makeAbortError(options.signal); + } + + const requestOptions: Record = { + method: options.method || "GET", + headers: options.headers || {}, + body: options.body, + tlsClientIdentifier: PPLX_PROFILE, + timeoutMilliseconds: options.timeoutMs ?? DEFAULT_TIMEOUT_MS, + followRedirects: true, + withRandomTLSExtensionOrder: true, + // Plumb the configured proxy through to the native binding. tls-client-node + // consults `proxyUrl` in the per-call options (it does NOT auto-pick up + // HTTP_PROXY / HTTPS_PROXY env), so callers / env have to be threaded in + // explicitly. See `resolveProxyUrl()` for the lookup order. + proxyUrl: resolveProxyUrl(options.proxyUrl), + }; + + if (options.stream) { + return await tlsFetchStreaming( + client, + url, + requestOptions, + options.streamEofSymbol, + options.signal ?? null, + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS + ); + } + + let tlsResponse: TlsResponseLike; + try { + tlsResponse = await raceWithTimeout( + client.request(url, requestOptions), + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) + HARD_TIMEOUT_GRACE_MS, + options.signal ?? null + ); + } catch (err) { + if (err instanceof TlsClientHangError) { + // The native binding is wedged — drop the singleton so the next + // request respawns a fresh client (and a fresh koffi load). + resetClientCache(); + } + throw err; + } + if (options.signal?.aborted) { + throw makeAbortError(options.signal); + } + return { + status: tlsResponse.status, + headers: toHeaders(tlsResponse.headers), + text: tlsResponse.body, + body: null, + }; +} + +function makeAbortError(signal: AbortSignal): Error { + const reason = signal.reason; + if (reason instanceof Error) return reason; + const err = new Error(typeof reason === "string" ? reason : "The operation was aborted"); + err.name = "AbortError"; + return err; +} + +function toHeaders(raw: Record): Headers { + const h = new Headers(); + for (const [k, vs] of Object.entries(raw || {})) { + for (const v of vs) h.append(k, v); + } + return h; +} + +/** + * Returns true if the response body is a Cloudflare challenge/interstitial page + * rather than a real Perplexity response. From VPS/datacenter IPs a valid cookie + * still gets a 403 "Just a moment..." HTML page; distinguishing it from a genuine + * auth failure lets the caller surface an actionable error (issue #2459). + * + * Exported so the executor and the connection validator share one detector. + */ +export function isCloudflareChallenge(text: string | null | undefined): boolean { + if (!text) return false; + return /just a moment|window\._cf_chl_opt|challenges\.cloudflare\.com|attention required|cf-chl/i.test( + text + ); +} + +// ─── Streaming via temp file ──────────────────────────────────────────────── +// tls-client-node's streaming primitive writes the response body chunk-by-chunk +// to a file path, terminating when the upstream sends `streamOutputEOFSymbol`. +// We tail the file from a worker and surface the bytes as a ReadableStream. + +async function tlsFetchStreaming( + client: { request: (url: string, opts: Record) => Promise }, + url: string, + requestOptions: Record, + eofSymbol = "[DONE]", + signal: AbortSignal | null = null, + hardTimeoutMs: number = DEFAULT_TIMEOUT_MS + HARD_TIMEOUT_GRACE_MS +): Promise { + const dir = await mkdtemp(join(tmpdir(), "pplx-stream-")); + const path = join(dir, `${randomUUID()}.sse`); + + const streamOpts = { + ...requestOptions, + streamOutputPath: path, + streamOutputBlockSize: 1024, + streamOutputEOFSymbol: eofSymbol, + }; + + // Kick off the request without awaiting — tls-client writes the body to + // `path` chunk-by-chunk while the call runs. The Promise resolves when the + // request fully completes (full body written). Wrapping in raceWithTimeout + // guarantees this promise eventually settles even if the koffi binding + // wedges; on hang we reset the singleton so the next request respawns. + let resetOnHang = true; + const requestPromise = raceWithTimeout( + client.request(url, streamOpts), + hardTimeoutMs, + signal + ).catch((err: unknown) => { + if (resetOnHang && err instanceof TlsClientHangError) { + resetClientCache(); + resetOnHang = false; + } + // Re-throw so downstream consumers (waitForContent, tailFile) observe + // the rejection and surface it instead of treating the stream as having + // ended cleanly. + throw err; + }); + + // Wait for the file to exist AND have at least one byte. tls-client-node + // creates the output file when the request starts, but the file can be + // empty for a brief window before the first body chunk lands — peeking + // during that window would return "" and misclassify the response as + // non-SSE, dropping us into the buffered-wait branch and silently turning + // a streaming request into a buffered one. Waiting for content avoids + // that race; if the request actually fails before producing any bytes, + // the timeout falls through to the requestPromise drain below (returning + // the real upstream status). + const ready = await waitForContent(path, 5_000, requestPromise); + if (!ready) { + const r = await requestPromise.catch( + (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike + ); + await cleanupTempPath(path); + return { + status: r.status, + headers: toHeaders(r.headers), + text: r.body, + body: null, + }; + } + + // Peek the first bytes to decide whether this looks like SSE. Anything + // that doesn't positively look like SSE (JSON `{...}`, HTML `<...>`, plain + // text rate-limit messages, Cloudflare challenge pages, etc.) gets surfaced + // as a non-streaming response so the executor sees the real upstream status + // and body — otherwise non-2xx error pages get silently treated as 200 OK + // and the SSE parser produces an empty completion. + const peek = await readFirstBytes(path, 256); + if (!looksLikeSse(peek)) { + const r = await requestPromise.catch( + (e) => ({ status: 502, headers: {}, body: String(e) }) as TlsResponseLike + ); + await cleanupTempPath(path); + return { + status: r.status, + headers: toHeaders(r.headers), + text: r.body, + body: null, + }; + } + + // Looks like SSE — start tailing. SSE bodies in practice are always 2xx; + // tls-client-node doesn't expose response status separately from full-body + // completion, so we report 200 and let the SSE parser consume the stream. + const stream = tailFile(path, eofSymbol, requestPromise, signal); + const headers = new Headers({ + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + }); + return { status: 200, headers, text: null, body: stream }; +} + +/** + * Returns true if the peeked response body looks like an SSE stream — i.e., + * begins (after any leading whitespace) with one of the SSE field markers + * (`data:`, `event:`, `id:`, `retry:`) or a comment line (`:`). + * + * Exported for tests. + */ +export function looksLikeSse(text: string): boolean { + const trimmed = text.replace(/^[\s\r\n]+/, ""); + if (!trimmed) return false; + if (trimmed.startsWith(":")) return true; + return /^(data|event|id|retry):/i.test(trimmed); +} + +async function cleanupTempPath(path: string): Promise { + await unlink(path).catch(() => {}); + const dir = path.substring(0, path.lastIndexOf("/")); + await rmdir(dir).catch(() => {}); +} + +async function readFirstBytes(path: string, n: number): Promise { + const fd = await open(path, "r"); + try { + const buf = Buffer.alloc(n); + const { bytesRead } = await fd.read(buf, 0, n, 0); + return buf.subarray(0, bytesRead).toString("utf8"); + } finally { + await fd.close().catch(() => {}); + } +} + +/** + * Wait for the streaming output file to exist AND contain at least one byte. + * Returns false if the request settles before any bytes arrive (so the caller + * can drain `requestPromise` and surface the real upstream status). Returns + * true as soon as the file has data — even one byte is enough for the SSE + * heuristic to give a useful answer. + */ +async function waitForContent( + path: string, + timeoutMs: number, + requestPromise: Promise +): Promise { + let requestSettled = false; + requestPromise.then( + () => { + requestSettled = true; + }, + () => { + requestSettled = true; + } + ); + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const s = await stat(path); + if (s.size > 0) return true; + } catch { + // file doesn't exist yet + } + // If the request finished without producing any bytes, no point waiting + // out the rest of the timeout — let the caller drain it. + if (requestSettled) return false; + await sleep(25); + } + return false; +} + +function tailFile( + path: string, + eofSymbol: string, + done: Promise, + signal: AbortSignal | null = null +): ReadableStream { + return new ReadableStream({ + async start(controller) { + const fd = await open(path, "r"); + const buf = Buffer.alloc(64 * 1024); + let offset = 0; + let finished = false; + let aborted = false; + let upstreamError: Error | null = null; + + // Track request settlement, capturing both fulfillment and rejection. + // Without the rejection branch, a mid-stream tls-client-node error + // becomes an unhandledRejection — the stream cleans up silently and + // the consumer sees what looks like a successful truncated response. + done.then( + () => { + finished = true; + }, + (err) => { + upstreamError = err instanceof Error ? err : new Error(String(err)); + finished = true; + } + ); + + // If the caller aborts, stop tailing immediately. + const onAbort = () => { + aborted = true; + }; + if (signal) { + if (signal.aborted) aborted = true; + else signal.addEventListener("abort", onAbort, { once: true }); + } + + let errored = false; + try { + while (!aborted) { + const { bytesRead } = await fd.read(buf, 0, buf.length, offset); + if (bytesRead > 0) { + const chunk = buf.subarray(0, bytesRead); + offset += bytesRead; + const text = chunk.toString("utf8"); + if (text.includes(eofSymbol)) { + const cutAt = text.indexOf(eofSymbol) + eofSymbol.length; + controller.enqueue(new Uint8Array(chunk.subarray(0, cutAt))); + break; + } + controller.enqueue(new Uint8Array(chunk)); + } else if (finished) { + // No more data and request completed. If the request rejected, + // surface the error so the consumer doesn't think the stream + // ended cleanly. + if (upstreamError) { + controller.error(upstreamError); + errored = true; + } + break; + } else { + await sleep(25); + } + } + } catch (err) { + controller.error(err); + errored = true; + } finally { + if (signal) signal.removeEventListener("abort", onAbort); + await fd.close().catch(() => {}); + await unlink(path).catch(() => {}); + const dir = path.substring(0, path.lastIndexOf("/")); + await rmdir(dir).catch(() => {}); + if (!errored) controller.close(); + } + }, + }); +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 107775e186..8a446a69e4 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -2948,8 +2948,9 @@ async function validatePerplexityWebProvider({ apiKey, providerSpecificData = {} Accept: "text/event-stream", Origin: "https://www.perplexity.ai", Referer: "https://www.perplexity.ai/", + // Firefox 148 — must match the firefox_148 TLS profile of perplexityTlsClient (issue #2459). "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/136.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", "X-App-ApiClient": "default", "X-App-ApiVersion": "client-1.11.0", ...(bearerToken @@ -2961,32 +2962,60 @@ async function validatePerplexityWebProvider({ apiKey, providerSpecificData = {} providerSpecificData ); - const response = await validationWrite("https://www.perplexity.ai/rest/sse/perplexity_ask", { - method: "POST", - headers, - body: JSON.stringify({ - query_str: "test", - params: { + // Perplexity is behind Cloudflare Enterprise which pins JA3/JA4 to a real + // browser handshake — plain fetch is challenged with a 403 page from + // VPS/datacenter IPs even with a valid cookie. Use the Firefox-fingerprinted + // TLS client so the validator's verdict reflects the cookie, not the IP (issue #2459). + const { tlsFetchPerplexity, isCloudflareChallenge, TlsClientUnavailableError } = + await import("@omniroute/open-sse/services/perplexityTlsClient.ts"); + + let response: { status: number; text: string | null }; + try { + response = await tlsFetchPerplexity("https://www.perplexity.ai/rest/sse/perplexity_ask", { + method: "POST", + headers, + body: JSON.stringify({ query_str: "test", - search_focus: "internet", - mode: "concise", - model_preference: "default", - sources: ["web"], - attachments: [], - frontend_uuid: crypto.randomUUID(), - frontend_context_uuid: crypto.randomUUID(), - version: "client-1.11.0", - language: "en-US", - timezone, - search_recency_filter: null, - is_incognito: true, - use_schematized_api: true, - last_backend_uuid: null, - }, - }), - }); + params: { + query_str: "test", + search_focus: "internet", + mode: "concise", + model_preference: "default", + sources: ["web"], + attachments: [], + frontend_uuid: crypto.randomUUID(), + frontend_context_uuid: crypto.randomUUID(), + version: "client-1.11.0", + language: "en-US", + timezone, + search_recency_filter: null, + is_incognito: true, + use_schematized_api: true, + last_backend_uuid: null, + }, + }), + timeoutMs: 30_000, + }); + } catch (err) { + if (err instanceof TlsClientUnavailableError) { + return { + valid: false, + error: `${err.message} perplexity-web requires it — without it Cloudflare blocks every request.`, + }; + } + throw err; + } if (response.status === 401 || response.status === 403) { + if (isCloudflareChallenge(response.text)) { + return { + valid: false, + error: + "Cloudflare is blocking connections from this server's IP (TLS fingerprint rejected). " + + "The session cookie may still be valid — install tls-client-node's native binary or route " + + "perplexity-web through a residential proxy.", + }; + } return { valid: false, error: @@ -2994,7 +3023,7 @@ async function validatePerplexityWebProvider({ apiKey, providerSpecificData = {} }; } - if (response.ok || (response.status >= 400 && response.status < 500)) { + if (response.status === 200 || (response.status >= 400 && response.status < 500)) { return { valid: true, error: null }; } diff --git a/tests/unit/perplexity-tls-client.test.ts b/tests/unit/perplexity-tls-client.test.ts new file mode 100644 index 0000000000..cad1b2af76 --- /dev/null +++ b/tests/unit/perplexity-tls-client.test.ts @@ -0,0 +1,42 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { isCloudflareChallenge, looksLikeSse, TlsClientUnavailableError } = + await import("../../open-sse/services/perplexityTlsClient.ts"); + +// Regression for #2459: a Cloudflare 403 challenge page must be distinguishable from a +// genuine auth failure so the executor/validator can surface an actionable error. + +test("#2459 isCloudflareChallenge detects 'Just a moment' interstitial", () => { + assert.equal(isCloudflareChallenge("Just a moment..."), true); +}); + +test("#2459 isCloudflareChallenge detects window._cf_chl_opt", () => { + assert.equal(isCloudflareChallenge(""), true); +}); + +test("#2459 isCloudflareChallenge detects challenges.cloudflare.com", () => { + assert.equal(isCloudflareChallenge('src="https://challenges.cloudflare.com/turnstile"'), true); +}); + +test("#2459 isCloudflareChallenge returns false for normal JSON / SSE / empty", () => { + assert.equal(isCloudflareChallenge('{"status":"ok"}'), false); + assert.equal(isCloudflareChallenge('data: {"text":"hi"}\n\n'), false); + assert.equal(isCloudflareChallenge(""), false); + assert.equal(isCloudflareChallenge(null), false); + assert.equal(isCloudflareChallenge(undefined), false); +}); + +test("#2459 looksLikeSse positive for data/event markers, negative for HTML", () => { + assert.equal(looksLikeSse("data: {}\n\n"), true); + assert.equal(looksLikeSse("event: message\n"), true); + assert.equal(looksLikeSse(": comment"), true); + assert.equal(looksLikeSse("Just a moment"), false); + assert.equal(looksLikeSse('{"json":true}'), false); +}); + +test("#2459 TlsClientUnavailableError has the expected name", () => { + const err = new TlsClientUnavailableError("native binary missing"); + assert.equal(err.name, "TlsClientUnavailableError"); + assert.ok(err instanceof Error); +}); diff --git a/tests/unit/perplexity-web.test.ts b/tests/unit/perplexity-web.test.ts index a04c2d00a7..58e4787262 100644 --- a/tests/unit/perplexity-web.test.ts +++ b/tests/unit/perplexity-web.test.ts @@ -6,6 +6,21 @@ import assert from "node:assert/strict"; const { PerplexityWebExecutor } = await import("../../open-sse/executors/perplexity-web.ts"); const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); +const { __setTlsFetchOverrideForTesting, TlsClientUnavailableError } = + await import("../../open-sse/services/perplexityTlsClient.ts"); + +// #2459: the executor now routes through tlsFetchPerplexity (Firefox TLS) instead of +// global fetch. Install one persistent bridge so the tests below can keep stubbing +// globalThis.fetch (returning a Response) and have it surface as a TlsFetchResult. +__setTlsFetchOverrideForTesting(async (url, opts) => { + const res = await (globalThis.fetch as any)(url, opts); + return { + status: res.status, + headers: res.headers, + text: res.status === 200 ? null : await res.text(), + body: res.status === 200 ? res.body : null, + }; +}); // ─── Helper: Build a mock SSE stream from Perplexity events ───────────────── @@ -25,14 +40,22 @@ function mockPplxStream(events) { }); } -// ─── Helper: Override global fetch for testing ────────────────────────────── +// ─── Helper: stub globalThis.fetch for testing ────────────────────────────── +// The persistent bridge above forwards tlsFetchPerplexity calls to globalThis.fetch, +// so stubbing fetch is still the way to mock Perplexity's upstream response. -function mockFetch(status, streamEvents) { +function mockFetch(status, streamEvents, bodyText) { const original = globalThis.fetch; - globalThis.fetch = async (url, opts) => { - return new Response(mockPplxStream(streamEvents), { + globalThis.fetch = async () => { + if (status === 200) { + return new Response(mockPplxStream(streamEvents), { + status, + headers: { "Content-Type": "text/event-stream" }, + }); + } + return new Response(bodyText ?? `{"error":"http ${status}"}`, { status, - headers: { "Content-Type": "text/event-stream" }, + headers: { "Content-Type": "text/html" }, }); }; return () => { @@ -750,3 +773,48 @@ test("Request: posts to correct Perplexity SSE endpoint", async () => { globalThis.fetch = original; } }); + +// ─── #2459: Cloudflare challenge vs genuine auth failure ───────────────────── + +test("Error: Cloudflare 403 challenge returns a distinct (non-cookie) error", async () => { + const restore = mockFetch(403, [], "Just a moment..."); + try { + const executor = new PerplexityWebExecutor(); + const result = await executor.execute({ + model: "pplx-auto", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: "valid-cookie" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + assert.equal(result.response.status, 403); + const json = (await result.response.json()) as any; + assert.match(json.error.message, /Cloudflare/i); + assert.ok(!/session-token/i.test(json.error.message), "must not blame the cookie"); + } finally { + restore(); + } +}); + +test("Error: TlsClientUnavailableError returns 502 with install hint", async () => { + const restore = mockFetchError(new TlsClientUnavailableError("native binary missing")); + try { + const executor = new PerplexityWebExecutor(); + const result = await executor.execute({ + model: "pplx-auto", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: "test-cookie" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + assert.equal(result.response.status, 502); + const json = (await result.response.json()) as any; + assert.match(json.error.message, /TLS client unavailable/i); + } finally { + restore(); + } +}); From 8f63c723966ae317267da33a3ba013fe8ca0ed47 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 04:02:58 -0300 Subject: [PATCH 005/112] docs(changelog): record #2390, #2446, #2454, #2459 bug fixes --- CHANGELOG.md | 7 +++++++ docs/i18n/ar/CHANGELOG.md | 7 +++++++ docs/i18n/az/CHANGELOG.md | 7 +++++++ docs/i18n/bg/CHANGELOG.md | 7 +++++++ docs/i18n/bn/CHANGELOG.md | 7 +++++++ docs/i18n/cs/CHANGELOG.md | 7 +++++++ docs/i18n/da/CHANGELOG.md | 7 +++++++ docs/i18n/de/CHANGELOG.md | 7 +++++++ docs/i18n/es/CHANGELOG.md | 7 +++++++ docs/i18n/fa/CHANGELOG.md | 7 +++++++ docs/i18n/fi/CHANGELOG.md | 7 +++++++ docs/i18n/fr/CHANGELOG.md | 7 +++++++ docs/i18n/gu/CHANGELOG.md | 7 +++++++ docs/i18n/he/CHANGELOG.md | 7 +++++++ docs/i18n/hi/CHANGELOG.md | 7 +++++++ docs/i18n/hu/CHANGELOG.md | 7 +++++++ docs/i18n/id/CHANGELOG.md | 7 +++++++ docs/i18n/in/CHANGELOG.md | 7 +++++++ docs/i18n/it/CHANGELOG.md | 7 +++++++ docs/i18n/ja/CHANGELOG.md | 7 +++++++ docs/i18n/ko/CHANGELOG.md | 7 +++++++ docs/i18n/mr/CHANGELOG.md | 7 +++++++ docs/i18n/ms/CHANGELOG.md | 7 +++++++ docs/i18n/nl/CHANGELOG.md | 7 +++++++ docs/i18n/no/CHANGELOG.md | 7 +++++++ docs/i18n/phi/CHANGELOG.md | 7 +++++++ docs/i18n/pl/CHANGELOG.md | 7 +++++++ docs/i18n/pt-BR/CHANGELOG.md | 7 +++++++ docs/i18n/pt/CHANGELOG.md | 7 +++++++ docs/i18n/ro/CHANGELOG.md | 7 +++++++ docs/i18n/ru/CHANGELOG.md | 7 +++++++ docs/i18n/sk/CHANGELOG.md | 7 +++++++ docs/i18n/sv/CHANGELOG.md | 7 +++++++ docs/i18n/sw/CHANGELOG.md | 7 +++++++ docs/i18n/ta/CHANGELOG.md | 7 +++++++ docs/i18n/te/CHANGELOG.md | 7 +++++++ docs/i18n/th/CHANGELOG.md | 7 +++++++ docs/i18n/tr/CHANGELOG.md | 7 +++++++ docs/i18n/uk-UA/CHANGELOG.md | 7 +++++++ docs/i18n/ur/CHANGELOG.md | 7 +++++++ docs/i18n/vi/CHANGELOG.md | 7 +++++++ docs/i18n/zh-CN/CHANGELOG.md | 7 +++++++ 42 files changed, 294 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1631eb2abf..0411435535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent` (same path already used for Anthropic `tool_result` blocks). ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate the heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was receiving `context-1m` and rejecting it with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Anthropic-native Claude OAuth passthrough (converts them to `redacted_thinking`), fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client (`perplexityTlsClient.ts`) so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation and execution now distinguish a Cloudflare block from an invalid session cookie instead of always blaming the cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + --- ## [3.8.1] — 2026-05-21 diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index c7eb743fba..30dcb242e8 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index 2e22b4d40f..f8e98cdae5 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index b81806c374..7d26a87ca1 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index e7250e504d..5a3a977bfb 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 5f0d6690e1..5db5a82291 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 2e470cb4c5..1425351d43 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 042d1a334d..e790d123e0 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index 920fa6e17a..6346742ed8 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index f0f388a659..9459bfdafa 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index a7a8b99203..3d9ea239f8 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index d7a4748a3b..843f3f234f 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index b1e3480184..b8d0506f9a 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 9a81272daf..0a5304cf0a 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 64e0e3d3c8..3d3c152fe4 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index 989c131268..a0d0619683 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 6e066b3023..e98c025d56 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index cdff8561e4..f48ff4779c 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 70900b5a07..99eec182a3 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index ebf906a992..7c4aeb2f77 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index 18e64bd9e2..ef08c3d825 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 3833a2a49f..42def759fb 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 03a5294e8e..af684904db 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index 7661ed7778..2ea8e51674 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index bbf45715fc..e1f941de3b 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 330a5e6c4e..b1425b8e4f 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 03638aeed7..45edca61c4 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 804127e213..67c4ca09bc 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 8cae4d40e6..7c06c52533 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 8b7bef443b..65dd5fc7bc 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index e5377997f6..bf231e6006 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index 16c1837dab..e329c33a7f 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index 8f94f974fc..69e8288fc3 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 7e192256b3..845d8e3c81 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index ae45a485d0..2f392a69ba 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index 8d56c098cc..0bb793a872 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index a924af11a9..29c69095cc 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index 3f7d715850..cec59deb76 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index cb6f45b621..bf4986691f 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 38cb13d0f8..9ad49065a8 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 891e6c0b1c..e9b0433956 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 6139519cba..53526b8d16 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -6,6 +6,13 @@ ## [Unreleased] +### 🔧 Bug Fixes + +- **fix(translator):** inject `omniroute_web_search` in the Responses-API flat tool shape (`{ type, name }`) when the target provider speaks the Responses API — previously it was always emitted in the Chat Completions nested shape (`{ type, function: { name } }`), and on the Responses→Responses passthrough path nothing flattened it, so Codex/relay upstreams rejected the request with `[400]: Missing required parameter: 'tools[0].name'.` ([#2390](https://github.com/diegosouzapw/OmniRoute/issues/2390)) +- **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) +- **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) +- **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) + ## [3.8.1] — 2026-05-20 ### 🔧 Bug Fixes & Refactors From 140a3d81ed84076f7c20699a643aee117e5ed70f Mon Sep 17 00:00:00 2001 From: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Date: Thu, 21 May 2026 11:47:06 +0200 Subject: [PATCH 006/112] fix: extract system role messages in semantic passthrough path + bump CLI wire image to v2.1.146 --- .env.example | 2 +- open-sse/config/anthropicHeaders.ts | 7 ++- open-sse/executors/claudeIdentity.ts | 7 ++- open-sse/handlers/chatCore.ts | 72 ++++++++++++++--------- open-sse/services/ccBridgeTransforms.ts | 2 +- open-sse/services/claudeCodeCompatible.ts | 6 +- 6 files changed, 59 insertions(+), 37 deletions(-) diff --git a/.env.example b/.env.example index 1bda4388c7..18b4b2c5fb 100644 --- a/.env.example +++ b/.env.example @@ -600,7 +600,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98 # Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup. # Update these when providers release new CLI versions to avoid blocks. -CLAUDE_USER_AGENT="claude-cli/2.1.145 (external, cli)" +CLAUDE_USER_AGENT="claude-cli/2.1.146 (external, cli)" CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)" GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1" ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0" diff --git a/open-sse/config/anthropicHeaders.ts b/open-sse/config/anthropicHeaders.ts index 7ddc7a86a3..5f708a5a80 100644 --- a/open-sse/config/anthropicHeaders.ts +++ b/open-sse/config/anthropicHeaders.ts @@ -12,6 +12,9 @@ const ANTHROPIC_BETA_BASE = Object.freeze([ "fast-mode-2026-02-01", "redact-thinking-2026-02-12", "token-efficient-tools-2026-03-28", + "advisor-tool-2026-03-01", + "extended-cache-ttl-2025-04-11", + "cache-diagnosis-2026-04-07", ]); const CLAUDE_OAUTH_EXTRA_BETAS = Object.freeze(["fine-grained-tool-streaming-2025-05-14"]); @@ -26,7 +29,7 @@ export const ANTHROPIC_BETA_CLAUDE_OAUTH = [ ...ANTHROPIC_BETA_BASE.slice(3), ].join(","); -export const CLAUDE_CLI_VERSION = "2.1.137"; +export const CLAUDE_CLI_VERSION = "2.1.146"; export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`; -export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.81.0"; +export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.94.0"; export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.3.0"; diff --git a/open-sse/executors/claudeIdentity.ts b/open-sse/executors/claudeIdentity.ts index 30e081acd9..a2fe491ddc 100644 --- a/open-sse/executors/claudeIdentity.ts +++ b/open-sse/executors/claudeIdentity.ts @@ -12,9 +12,9 @@ import { createHash, randomBytes, randomUUID } from "node:crypto"; // ---------- Versions ------------------------------------------------------ -export const CLAUDE_CODE_VERSION = "2.1.131"; +export const CLAUDE_CODE_VERSION = "2.1.146"; /** Bundled @anthropic-ai/sdk version for the pinned CLI release. */ -export const CLAUDE_CODE_STAINLESS_VERSION = "0.81.0"; +export const CLAUDE_CODE_STAINLESS_VERSION = "0.94.0"; // ---------- Stainless OS / Arch / Runtime -------------------------------- @@ -318,7 +318,8 @@ export function selectBetaFlags(body: Record | null | undefined flags.push( "advanced-tool-use-2025-11-20", "effort-2025-11-24", - "extended-cache-ttl-2025-04-11" + "extended-cache-ttl-2025-04-11", + "cache-diagnosis-2026-04-07" ); } return flags.join(","); diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 74a059e2e3..a2c24bea2b 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2577,6 +2577,45 @@ export async function handleChatCore({ content?: unknown; }; + /** + * Lightweight extraction: only lifts role:"system" messages to the top-level + * `system` parameter. Unlike normalizeClaudeUpstreamMessages, this does NOT + * convert file/document blocks, drop unknown types, or change tool history. + * Used in the semantic passthrough path where Claude Code's native payload + * structure must be preserved — only memory injection (which prepends a + * system message) needs this correction. + */ + const extractSystemRoleMessages = (payload: Record) => { + if (!Array.isArray(payload.messages)) return; + const messages = payload.messages as ClaudeMessage[]; + const systemMessages = messages.filter((m) => m.role === "system"); + if (systemMessages.length === 0) return; + + const extraBlocks: ClaudeContentBlock[] = []; + for (const sm of systemMessages) { + if (typeof sm.content === "string" && sm.content.length > 0) { + extraBlocks.push({ type: "text", text: sm.content }); + } else if (Array.isArray(sm.content)) { + for (const block of sm.content as ClaudeContentBlock[]) { + if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { + extraBlocks.push(block); + } + } + } + } + if (extraBlocks.length > 0) { + const existingSystem = payload.system; + if (typeof existingSystem === "string" && existingSystem.length > 0) { + payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; + } else if (Array.isArray(existingSystem)) { + payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; + } else { + payload.system = extraBlocks; + } + } + payload.messages = messages.filter((m) => m.role !== "system"); + }; + const normalizeClaudeUpstreamMessages = ( payload: Record, options?: { preserveToolResultBlocks?: boolean } @@ -2586,33 +2625,8 @@ export async function handleChatCore({ let messages = payload.messages as ClaudeMessage[]; // Extract system role messages (Issue #1797) - const systemMessages = messages.filter((m) => m.role === "system"); - if (systemMessages.length > 0) { - const extraBlocks: ClaudeContentBlock[] = []; - for (const sm of systemMessages) { - if (typeof sm.content === "string" && sm.content.length > 0) { - extraBlocks.push({ type: "text", text: sm.content }); - } else if (Array.isArray(sm.content)) { - for (const block of sm.content as ClaudeContentBlock[]) { - if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { - extraBlocks.push(block); - } - } - } - } - if (extraBlocks.length > 0) { - const existingSystem = payload.system; - if (typeof existingSystem === "string" && existingSystem.length > 0) { - payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; - } else if (Array.isArray(existingSystem)) { - payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; - } else { - payload.system = extraBlocks; - } - } - messages = messages.filter((m) => m.role !== "system"); - payload.messages = messages; - } + extractSystemRoleMessages(payload); + messages = payload.messages as ClaudeMessage[]; // Anthropic rejects empty text blocks in native Messages payloads. for (const msg of messages) { @@ -2745,6 +2759,10 @@ export async function handleChatCore({ if (!isClaudeCodeSemanticPassthrough) { normalizeClaudeUpstreamMessages(translatedBody, { preserveToolResultBlocks: true }); } else { + // Lightweight system role extraction only — preserves all other + // Claude Code payload structure (documents, tool history, etc.) + // while correcting memory injection's role:"system" message. + extractSystemRoleMessages(translatedBody); log?.debug?.("FORMAT", "claude-code semantic passthrough enabled"); } diff --git a/open-sse/services/ccBridgeTransforms.ts b/open-sse/services/ccBridgeTransforms.ts index 065b613a33..83c5138703 100644 --- a/open-sse/services/ccBridgeTransforms.ts +++ b/open-sse/services/ccBridgeTransforms.ts @@ -114,7 +114,7 @@ export const CCH_SALT = "59cf53e54c78"; /** Character positions sampled from the first user message text. */ export const CCH_POSITIONS = [4, 7, 20] as const; /** Default `cc_version=` value embedded in the billing header. */ -export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.137"; +export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.146"; /** Identity sentinel prepended for Claude Agent SDK callers. */ export const CLAUDE_AGENT_SDK_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 6b7bd98b04..1276626921 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -39,9 +39,9 @@ export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = [ "interleaved-thinking-2025-05-14", "effort-2025-11-24", ].join(","); -export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.137"; -export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.137 (external, sdk-cli)"; -export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.81.0"; +export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.146"; +export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.146 (external, sdk-cli)"; +export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.94.0"; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v24.3.0"; export const CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07"; const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [ From 7e05abd398514aabbab9d9247c4538b31108e7fd Mon Sep 17 00:00:00 2001 From: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Date: Thu, 21 May 2026 12:26:07 +0200 Subject: [PATCH 007/112] fix: extract system role messages in semantic passthrough path + add test --- open-sse/handlers/chatCore.ts | 74 +++++++------- tests/unit/system-role-extraction.test.ts | 119 ++++++++++++++++++++++ 2 files changed, 154 insertions(+), 39 deletions(-) create mode 100644 tests/unit/system-role-extraction.test.ts diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index a2c24bea2b..aa918d2f8d 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1242,6 +1242,41 @@ function isCopilotClient( return false; } +export function extractSystemRoleMessages(payload: Record): void { + if (!Array.isArray(payload.messages)) return; + const messages = payload.messages as Array<{ role?: unknown; content?: unknown }>; + const systemMessages = messages.filter( + (m) => typeof m.role === "string" && m.role.toLowerCase() === "system" + ); + if (systemMessages.length === 0) return; + + const extraBlocks: Array> = []; + for (const sm of systemMessages) { + if (typeof sm.content === "string" && sm.content.length > 0) { + extraBlocks.push({ type: "text", text: sm.content }); + } else if (Array.isArray(sm.content)) { + for (const block of sm.content as Array>) { + if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { + extraBlocks.push({ ...block }); + } + } + } + } + if (extraBlocks.length > 0) { + const existingSystem = payload.system; + if (typeof existingSystem === "string" && existingSystem.length > 0) { + payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; + } else if (Array.isArray(existingSystem)) { + payload.system = [...(existingSystem as Array>), ...extraBlocks]; + } else { + payload.system = extraBlocks; + } + } + payload.messages = messages.filter( + (m) => typeof m.role !== "string" || m.role.toLowerCase() !== "system" + ); +} + export async function handleChatCore({ body, modelInfo, @@ -2577,45 +2612,6 @@ export async function handleChatCore({ content?: unknown; }; - /** - * Lightweight extraction: only lifts role:"system" messages to the top-level - * `system` parameter. Unlike normalizeClaudeUpstreamMessages, this does NOT - * convert file/document blocks, drop unknown types, or change tool history. - * Used in the semantic passthrough path where Claude Code's native payload - * structure must be preserved — only memory injection (which prepends a - * system message) needs this correction. - */ - const extractSystemRoleMessages = (payload: Record) => { - if (!Array.isArray(payload.messages)) return; - const messages = payload.messages as ClaudeMessage[]; - const systemMessages = messages.filter((m) => m.role === "system"); - if (systemMessages.length === 0) return; - - const extraBlocks: ClaudeContentBlock[] = []; - for (const sm of systemMessages) { - if (typeof sm.content === "string" && sm.content.length > 0) { - extraBlocks.push({ type: "text", text: sm.content }); - } else if (Array.isArray(sm.content)) { - for (const block of sm.content as ClaudeContentBlock[]) { - if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { - extraBlocks.push(block); - } - } - } - } - if (extraBlocks.length > 0) { - const existingSystem = payload.system; - if (typeof existingSystem === "string" && existingSystem.length > 0) { - payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; - } else if (Array.isArray(existingSystem)) { - payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; - } else { - payload.system = extraBlocks; - } - } - payload.messages = messages.filter((m) => m.role !== "system"); - }; - const normalizeClaudeUpstreamMessages = ( payload: Record, options?: { preserveToolResultBlocks?: boolean } diff --git a/tests/unit/system-role-extraction.test.ts b/tests/unit/system-role-extraction.test.ts new file mode 100644 index 0000000000..306fb7feef --- /dev/null +++ b/tests/unit/system-role-extraction.test.ts @@ -0,0 +1,119 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { extractSystemRoleMessages } from "../../open-sse/handlers/chatCore.ts"; + +test("extractSystemRoleMessages moves role=system to top-level system", () => { + const payload = { + messages: [ + { role: "system", content: "Memory context: foo" }, + { role: "user", content: "hello" }, + { role: "assistant", content: "hi" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 2); + assert.equal(payload.messages[0].role, "user"); + assert.deepEqual(payload.system, [{ type: "text", text: "Memory context: foo" }]); +}); + +test("extractSystemRoleMessages merges with existing top-level system string", () => { + const payload = { + system: "You are Claude.", + messages: [ + { role: "system", content: "Memory context: bar" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [ + { type: "text", text: "You are Claude." }, + { type: "text", text: "Memory context: bar" }, + ]); +}); + +test("extractSystemRoleMessages merges with existing top-level system array", () => { + const payload = { + system: [{ type: "text", text: "Existing system" }], + messages: [ + { role: "system", content: "Memory context: baz" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [ + { type: "text", text: "Existing system" }, + { type: "text", text: "Memory context: baz" }, + ]); +}); + +test("extractSystemRoleMessages does nothing when no system role messages", () => { + const payload = { + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: "hi" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 2); + assert.equal(payload.system, undefined); +}); + +test("extractSystemRoleMessages handles non-array messages gracefully", () => { + const payload = { messages: "not-an-array" }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages, "not-an-array"); +}); + +test("extractSystemRoleMessages handles empty messages array", () => { + const payload = { messages: [] }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 0); +}); + +test("extractSystemRoleMessages handles case-insensitive role System", () => { + const payload = { + messages: [ + { role: "System", content: "Memory context: caps" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [{ type: "text", text: "Memory context: caps" }]); +}); + +test("extractSystemRoleMessages drops empty text content from system messages", () => { + const payload = { + messages: [ + { role: "system", content: "" }, + { role: "system", content: "valid" }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [{ type: "text", text: "valid" }]); +}); + +test("extractSystemRoleMessages handles system messages with array content", () => { + const payload = { + messages: [ + { + role: "system", + content: [ + { type: "text", text: "Block 1" }, + { type: "text", text: "Block 2" }, + ], + }, + { role: "user", content: "hello" }, + ], + }; + extractSystemRoleMessages(payload); + assert.equal(payload.messages.length, 1); + assert.deepEqual(payload.system, [ + { type: "text", text: "Block 1" }, + { type: "text", text: "Block 2" }, + ]); +}); From 2a0d20945de9bf75d9b48ecb5d38142ee8a0b3d3 Mon Sep 17 00:00:00 2001 From: Automation Date: Thu, 21 May 2026 14:37:26 +0200 Subject: [PATCH 008/112] fix(@omniroute/opencode-provider): include limit.context in model entries for OpenCode context window detection OpenCode determines model context windows by reading limit.context from opencode.json model entries. The provider was not emitting this field, so all OmniRoute models appeared with an unknown (0) context window in OpenCode, preventing proper compaction and overflow detection. - Add limit.context to OpenCodeModelEntry interface - Add OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS map (200K Claude / 1M Gemini) - Include limit.context when generating model entries - Extend fetchLiveModels to capture context_length from /v1/models - 5 new tests covering context length coverage, JSON serialisation, unknown model fallback, and live model fetch Closes #2481 --- @omniroute/opencode-provider/src/index.ts | 50 ++++++++++++- .../opencode-provider/tests/index.test.ts | 72 +++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index 4e2f070a19..454f45012c 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -78,6 +78,20 @@ export interface ModelCapabilities { tool_call?: boolean; } +/** + * Default per-model context window sizes (tokens) for the curated default catalog. + * Matches the context lengths used by OmniRoute's provider registry. + */ +export const OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS: Record = { + "cc/claude-opus-4-7": 200_000, + "cc/claude-sonnet-4-6": 200_000, + "cc/claude-haiku-4-5-20251001": 200_000, + "claude-opus-4-5-thinking": 200_000, + "claude-sonnet-4-5-thinking": 200_000, + "gemini-3.1-pro-high": 1_000_000, + "gemini-3-flash": 1_000_000, +}; + /** * Default per-model capability hints for the curated default catalog. * @@ -141,6 +155,19 @@ export interface OpenCodeModelEntry { reasoning?: boolean; temperature?: boolean; tool_call?: boolean; + /** + * Context window limit. OpenCode reads this to determine usable context + * length for compaction, overflow detection, and router decisions. + * Maps to `limit.context` in OpenCode's provider config schema. + */ + limit?: { + /** Maximum context length in tokens (e.g. 200000 for Claude, 1000000 for Gemini). */ + context: number; + /** Optional per-request max input tokens. */ + input?: number; + /** Optional max output tokens. */ + output?: number; + }; } export interface OpenCodeProviderEntry { @@ -235,6 +262,14 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open if (typeof merged.reasoning === "boolean") entry.reasoning = merged.reasoning; if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature; if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call; + + // Include context window limit when known — OpenCode reads this to + // determine usable context length for compaction & overflow detection. + const contextLength = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; + if (typeof contextLength === "number" && contextLength > 0) { + entry.limit = { context: contextLength }; + } + models[id] = entry; } @@ -445,6 +480,8 @@ async function fetchJSON(url: string, apiKey: string, timeoutMs: number): Pro export interface OmniRouteLiveModel { id: string; name: string; + /** Context window length in tokens (e.g. 200000 for Claude, 1000000 for Gemini). */ + contextLength?: number; } /** @@ -514,7 +551,18 @@ export async function fetchLiveModels( ? r.display_name.trim() : id; - models.push({ id, name: name || id }); + // Extract context_length from OmniRoute's /v1/models response. + // OmniRoute returns context_length in snake_case for both synced + // models (with inputTokenLimit) and custom models; the catalog's + // getDefaultContextFallback also injects it from registry defaults. + const contextLength = + typeof r.context_length === "number" && r.context_length > 0 + ? r.context_length + : typeof r.max_context_window_tokens === "number" && r.max_context_window_tokens > 0 + ? r.max_context_window_tokens + : undefined; + + models.push({ id, name: name || id, ...(contextLength ? { contextLength } : {}) }); } return models; diff --git a/@omniroute/opencode-provider/tests/index.test.ts b/@omniroute/opencode-provider/tests/index.test.ts index 324ae68bfa..9e14ba3bff 100644 --- a/@omniroute/opencode-provider/tests/index.test.ts +++ b/@omniroute/opencode-provider/tests/index.test.ts @@ -15,6 +15,7 @@ import { mergeIntoExistingConfig, normalizeBaseURL, OMNIROUTE_DEFAULT_MODEL_CAPABILITIES, + OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS, OMNIROUTE_DEFAULT_OPENCODE_MODELS, OMNIROUTE_MCP_DEFAULT_SCOPES, OMNIROUTE_PROVIDER_NPM, @@ -394,6 +395,77 @@ test("OMNIROUTE_DEFAULT_OPENCODE_MODELS includes cc/ prefixed models", () => { assert.ok(defaults.length >= 7, "should have at least 7 models"); }); +test("OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS covers every default model id", () => { + for (const id of OMNIROUTE_DEFAULT_OPENCODE_MODELS) { + const ctx = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; + assert.ok( + typeof ctx === "number" && ctx > 0, + `default context_length for ${id} missing — should be a positive number` + ); + // Sanity: context should be at least 8K, at most 2M tokens + assert.ok(ctx >= 8_000, `${id} context_length ${ctx} seems too low`); + assert.ok(ctx <= 2_000_000, `${id} context_length ${ctx} seems too high`); + } +}); + +test("createOmniRouteProvider emits limit.context on default model entries", () => { + const provider = createOmniRouteProvider({ + baseURL: "http://localhost:20128", + apiKey: "sk_omniroute", + }); + const entry = provider.models["cc/claude-opus-4-7"]; + assert.ok(entry.limit, "model entry should have a limit field"); + assert.equal(entry.limit!.context, 200_000); +}); + +test("createOmniRouteProvider omits limit.context for unknown model ids", () => { + const provider = createOmniRouteProvider({ + baseURL: "http://localhost:20128", + apiKey: "sk_omniroute", + models: ["completely-unknown-model"], + }); + const entry = provider.models["completely-unknown-model"]; + assert.equal(entry.limit, undefined); +}); + +test("createOmniRouteProvider serialises limit.context to JSON", () => { + const provider = createOmniRouteProvider({ + baseURL: "http://localhost:20128", + apiKey: "sk_omniroute", + }); + const round = JSON.parse(JSON.stringify(provider)); + for (const id of OMNIROUTE_DEFAULT_OPENCODE_MODELS) { + const expectedContext = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; + assert.equal( + round.models[id].limit?.context, + expectedContext, + `${id} should serialise limit.context=${expectedContext}` + ); + } +}); + +test("fetchLiveModels extracts context_length from snake_case field", async () => { + const { url, close } = await startMockServer(() => ({ + data: [ + { id: "cc/claude-opus-4-7", name: "Claude Opus 4.7", context_length: 200_000 }, + { id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro", context_length: 1_000_000 }, + { id: "no-context", name: "No Context" }, + ], + })); + try { + const models = await fetchLiveModels(url, "sk_test"); + const claude = models.find((m) => m.id === "cc/claude-opus-4-7"); + assert.ok(claude, "claude model should be present"); + assert.equal(claude!.contextLength, 200_000); + const gemini = models.find((m) => m.id === "gemini-3.1-pro-high"); + assert.equal(gemini!.contextLength, 1_000_000); + const noCtx = models.find((m) => m.id === "no-context"); + assert.equal(noCtx!.contextLength, undefined); + } finally { + close(); + } +}); + test("OMNIROUTE_DEFAULT_MODEL_CAPABILITIES covers every default model id", () => { for (const id of OMNIROUTE_DEFAULT_OPENCODE_MODELS) { const caps = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id]; From 2e85d80adfbc62c416d148b9727c8b262b3f5ad9 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 09:49:47 -0300 Subject: [PATCH 009/112] fix(validation): guard non-string apiKey/modelsUrl in connection test (#2463) A corrupted or mis-typed credential (non-string apiKey, or a non-string modelsUrl from providerSpecificData/registry) could throw 'TypeError: ... is not a function' when validation called .startsWith()/.trim() during a provider connection test. Adds typeof guards in validateOpenAILikeProvider, validateGeminiLikeProvider and validateSnowflakeProvider so validation returns a clean { valid } result instead of crashing. Does not pinpoint the NVIDIA NIM e.startsWith report (needs a stack trace), but hardens the whole class. --- CHANGELOG.md | 1 + docs/i18n/ar/CHANGELOG.md | 1 + docs/i18n/az/CHANGELOG.md | 1 + docs/i18n/bg/CHANGELOG.md | 1 + docs/i18n/bn/CHANGELOG.md | 1 + docs/i18n/cs/CHANGELOG.md | 1 + docs/i18n/da/CHANGELOG.md | 1 + docs/i18n/de/CHANGELOG.md | 1 + docs/i18n/es/CHANGELOG.md | 1 + docs/i18n/fa/CHANGELOG.md | 1 + docs/i18n/fi/CHANGELOG.md | 1 + docs/i18n/fr/CHANGELOG.md | 1 + docs/i18n/gu/CHANGELOG.md | 1 + docs/i18n/he/CHANGELOG.md | 1 + docs/i18n/hi/CHANGELOG.md | 1 + docs/i18n/hu/CHANGELOG.md | 1 + docs/i18n/id/CHANGELOG.md | 1 + docs/i18n/in/CHANGELOG.md | 1 + docs/i18n/it/CHANGELOG.md | 1 + docs/i18n/ja/CHANGELOG.md | 1 + docs/i18n/ko/CHANGELOG.md | 1 + docs/i18n/mr/CHANGELOG.md | 1 + docs/i18n/ms/CHANGELOG.md | 1 + docs/i18n/nl/CHANGELOG.md | 1 + docs/i18n/no/CHANGELOG.md | 1 + docs/i18n/phi/CHANGELOG.md | 1 + docs/i18n/pl/CHANGELOG.md | 1 + docs/i18n/pt-BR/CHANGELOG.md | 1 + docs/i18n/pt/CHANGELOG.md | 1 + docs/i18n/ro/CHANGELOG.md | 1 + docs/i18n/ru/CHANGELOG.md | 1 + docs/i18n/sk/CHANGELOG.md | 1 + docs/i18n/sv/CHANGELOG.md | 1 + docs/i18n/sw/CHANGELOG.md | 1 + docs/i18n/ta/CHANGELOG.md | 1 + docs/i18n/te/CHANGELOG.md | 1 + docs/i18n/th/CHANGELOG.md | 1 + docs/i18n/tr/CHANGELOG.md | 1 + docs/i18n/uk-UA/CHANGELOG.md | 1 + docs/i18n/ur/CHANGELOG.md | 1 + docs/i18n/vi/CHANGELOG.md | 1 + docs/i18n/zh-CN/CHANGELOG.md | 1 + src/lib/providers/validation.ts | 9 +++-- .../provider-validation-hardening.test.ts | 35 +++++++++++++++++++ 44 files changed, 83 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0411435535..2c2ec57457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent` (same path already used for Anthropic `tool_result` blocks). ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate the heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was receiving `context-1m` and rejecting it with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Anthropic-native Claude OAuth passthrough (converts them to `redacted_thinking`), fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client (`perplexityTlsClient.ts`) so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation and execution now distinguish a Cloudflare block from an invalid session cookie instead of always blaming the cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) --- diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 30dcb242e8..a0dc4763f7 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index f8e98cdae5..7ffb64f193 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index 7d26a87ca1..59c22fddc3 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 5a3a977bfb..b8aa9541ea 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 5db5a82291..5cef7dbfb0 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 1425351d43..3bb7430fe3 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index e790d123e0..d1f51f1b3a 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index 6346742ed8..4967486824 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index 9459bfdafa..d0527b9457 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 3d9ea239f8..3908a5d7be 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index 843f3f234f..5dfdd5f7b3 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index b8d0506f9a..09ba8b41e1 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 0a5304cf0a..358cc5c7a9 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 3d3c152fe4..a85a8d8a03 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index a0d0619683..e3bf9fb75d 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index e98c025d56..a696808d9e 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index f48ff4779c..91361c5fc7 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 99eec182a3..ffca6711c3 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 7c4aeb2f77..0a0cd46212 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index ef08c3d825..e2033281a9 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 42def759fb..c280e4480f 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index af684904db..5ad912fdc3 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index 2ea8e51674..5198a6435d 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index e1f941de3b..42de817607 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index b1425b8e4f..85c171d598 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 45edca61c4..daf5037833 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 67c4ca09bc..954fdbe97d 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 7c06c52533..696f7b1ac7 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 65dd5fc7bc..a8a96cac5c 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index bf231e6006..d0e037b1ab 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index e329c33a7f..90560ef3cb 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index 69e8288fc3..6598a6f5e8 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 845d8e3c81..6a12e325a0 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index 2f392a69ba..c0047505c9 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index 0bb793a872..a424891cd8 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index 29c69095cc..e5b4f79fd7 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index cec59deb76..38918ac85a 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index bf4986691f..96385ae654 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 9ad49065a8..7af521283d 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index e9b0433956..702717f69e 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 53526b8d16..0aa68addb4 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -12,6 +12,7 @@ - **fix(kiro):** serialize non-string `role:"tool"` message content before sending to CodeWhisperer — structured/array tool output was collapsing to `content:[{ text: "" }]`, which Kiro rejects with `400 Improperly formed request`. Now reuses the shared `serializeToolResultContent`. ([#2446](https://github.com/diegosouzapw/OmniRoute/issues/2446)) - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) +- **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) ## [3.8.1] — 2026-05-20 diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 8a446a69e4..b36c209e34 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -304,7 +304,10 @@ async function validateOpenAILikeProvider({ isLocal = false, }: any) { try { - const customModelsUrl = modelsUrl?.trim() || ""; + // Guard against a non-string modelsUrl reaching .trim()/.startsWith() — a malformed + // providerSpecificData / registry value would otherwise throw a TypeError mid-validation + // ("trim is not a function" / "startsWith is not a function"). See #2463 class. + const customModelsUrl = (typeof modelsUrl === "string" ? modelsUrl.trim() : "") || ""; const endpointUrl = customModelsUrl ? customModelsUrl.startsWith("http") ? customModelsUrl @@ -754,7 +757,7 @@ async function validateGeminiLikeProvider({ if (authType === "header" || authType === "apikey") { headers["x-goog-api-key"] = apiKey; - } else if (authType === "oauth" || apiKey.startsWith("ya29.")) { + } else if (authType === "oauth" || (typeof apiKey === "string" && apiKey.startsWith("ya29."))) { headers["Authorization"] = `Bearer ${apiKey}`; } @@ -1179,7 +1182,7 @@ async function validateSnowflakeProvider({ apiKey, providerSpecificData = {} }: return { valid: false, error: "Missing base URL" }; } - const usesProgrammaticAccessToken = apiKey.startsWith("pat/"); + const usesProgrammaticAccessToken = typeof apiKey === "string" && apiKey.startsWith("pat/"); return validateDirectChatProvider({ url: normalizeSnowflakeChatUrl(baseUrl), headers: { diff --git a/tests/unit/provider-validation-hardening.test.ts b/tests/unit/provider-validation-hardening.test.ts index ea30b87add..8858c87dbd 100644 --- a/tests/unit/provider-validation-hardening.test.ts +++ b/tests/unit/provider-validation-hardening.test.ts @@ -128,3 +128,38 @@ test("Claude Code compatible validation surfaces bridge connection failures", as assert.equal(result.valid, false); assert.match(result.error, /bridge failed/i); }); + +// Regression for the non-string-input crash class surfaced by #2463 +// ("e.startsWith is not a function" during a connection test). A non-string +// apiKey / modelsUrl must never throw a TypeError mid-validation — it should +// return a clean { valid: boolean } result. + +test("#2463 snowflake validation does not throw on non-string apiKey", async () => { + globalThis.fetch = async () => new Response(JSON.stringify({ data: [] }), { status: 200 }); + const result = await validateProviderApiKey({ + provider: "snowflake", + apiKey: 12345 as any, // simulates a corrupted / mis-typed credential + providerSpecificData: { baseUrl: "https://acct.snowflakecomputing.com" }, + }); + assert.equal(typeof result.valid, "boolean"); +}); + +test("#2463 gemini validation does not throw on non-string apiKey", async () => { + globalThis.fetch = async () => new Response(JSON.stringify({ data: [] }), { status: 200 }); + const result = await validateProviderApiKey({ + provider: "gemini", + apiKey: null as any, + providerSpecificData: {}, + }); + assert.equal(typeof result.valid, "boolean"); +}); + +test("#2463 openai-compatible validation does not throw on non-string modelsUrl", async () => { + globalThis.fetch = async () => new Response(JSON.stringify({ data: [] }), { status: 200 }); + const result = await validateProviderApiKey({ + provider: "openai-compatible-nonstring-modelsurl", + apiKey: "sk-test", + providerSpecificData: { baseUrl: "https://compat.example.com/v1", modelsUrl: 999 as any }, + }); + assert.equal(typeof result.valid, "boolean"); +}); From 0ade48bd041d2e25c5247b7e8921c3a13d8e2a67 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 21 May 2026 10:35:24 -0300 Subject: [PATCH 010/112] fix(security): replace Math.random with crypto.randomUUID in generateTaskId/ActivityId and fix URL hostname check in test (#2461) (#2489) Co-authored-by: diegosouzapw --- src/lib/cloudAgent/baseAgent.ts | 4 ++-- tests/unit/antigravity-discovery-bootstrap.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/cloudAgent/baseAgent.ts b/src/lib/cloudAgent/baseAgent.ts index 9f5689b90b..26c2defb59 100644 --- a/src/lib/cloudAgent/baseAgent.ts +++ b/src/lib/cloudAgent/baseAgent.ts @@ -86,10 +86,10 @@ export abstract class CloudAgentBase { } protected generateTaskId(): string { - return `task_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + return `task_${Date.now()}_${crypto.randomUUID().replace(/-/g, "").substring(0, 9)}`; } protected generateActivityId(): string { - return `act_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + return `act_${Date.now()}_${crypto.randomUUID().replace(/-/g, "").substring(0, 9)}`; } } diff --git a/tests/unit/antigravity-discovery-bootstrap.test.ts b/tests/unit/antigravity-discovery-bootstrap.test.ts index 3d0aa82f98..9f211e65e9 100644 --- a/tests/unit/antigravity-discovery-bootstrap.test.ts +++ b/tests/unit/antigravity-discovery-bootstrap.test.ts @@ -152,7 +152,7 @@ describe("ensureAntigravityProjectAssigned", () => { const mockFetch = async (url: string, _init?: RequestInit): Promise => { hitUrls.push(url); - if (url.includes("daily-cloudcode-pa.googleapis.com")) { + if (new URL(url).hostname === "daily-cloudcode-pa.googleapis.com") { // First URL fails return new Response("not found", { status: 404 }); } From 9989ac799fb9f05c03e8d24fc1a09ef503ac93d1 Mon Sep 17 00:00:00 2001 From: Automation Date: Thu, 21 May 2026 15:54:32 +0200 Subject: [PATCH 011/112] fix(combo): clarify log message when combo target is skipped due to unavailable credentials The combo loop log messages misleadingly said '(all accounts in cooldown)' when the actual reason could be model exclusion, rate-limiting, or other credential unavailability. Updated to accurately describe the real reason. --- open-sse/services/combo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 4b1a07f7a3..f24750e7ea 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -2042,11 +2042,11 @@ export async function handleComboChat({ continue; } - // Pre-check: skip models where all accounts are in cooldown + // Pre-check: skip models where no credentials are available (excluded, rate-limited, or unavailable) if (isModelAvailable) { const available = await isModelAvailable(modelStr, target); if (!available) { - log.info("COMBO", `Skipping ${modelStr} (all accounts in cooldown)`); + log.info("COMBO", `Skipping ${modelStr} — no credentials available or model excluded`); if (i > 0) fallbackCount++; continue; } @@ -2450,7 +2450,7 @@ async function handleRoundRobinCombo({ if (isModelAvailable) { const available = await isModelAvailable(modelStr, target); if (!available) { - log.info("COMBO-RR", `Skipping ${modelStr} (all accounts in cooldown)`); + log.info("COMBO-RR", `Skipping ${modelStr} — no credentials available or model excluded`); if (offset > 0) fallbackCount++; continue; } From 298578fec1eb7bcb76df5132fb338a8b15c3d61a Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:51:05 -0300 Subject: [PATCH 012/112] fix(cli): mark bin/omniroute.mjs executable (#2469) --- bin/omniroute.mjs | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/omniroute.mjs diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs old mode 100644 new mode 100755 From afa48c64ca74caaa53e1fd31ecb56cb6b70f1825 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:52:37 -0300 Subject: [PATCH 013/112] fix(settings): append Global System Prompt after provider/agent instructions (#2468) --- open-sse/services/systemPrompt.ts | 13 ++++++++----- tests/unit/system-prompt.test.ts | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/open-sse/services/systemPrompt.ts b/open-sse/services/systemPrompt.ts index f584a011aa..95dce9100a 100644 --- a/open-sse/services/systemPrompt.ts +++ b/open-sse/services/systemPrompt.ts @@ -44,21 +44,24 @@ export function injectSystemPrompt(body, promptText = null) { const sysIdx = result.messages.findIndex((m) => m.role === "system" || m.role === "developer"); result.messages = [...result.messages]; if (sysIdx >= 0) { - // Prepend to existing system message + // Append after existing system content so the global prompt is the FINAL + // instruction — provider/agent system blocks (Kiro, OpenCode, Hermes, etc.) + // are injected into the system message later, and recency bias means the + // user's global prompt must come after them to take priority (#2468). const msg = { ...result.messages[sysIdx] }; - msg.content = text + "\n\n" + (msg.content || ""); + msg.content = (msg.content || "") + "\n\n" + text; result.messages[sysIdx] = msg; } else { result.messages = [{ role: "system", content: text }, ...result.messages]; } } - // Claude format (system field) + // Claude format (system field) — append for the same reason as above (#2468). if (result.system !== undefined) { if (typeof result.system === "string") { - result.system = text + "\n\n" + result.system; + result.system = result.system + "\n\n" + text; } else if (Array.isArray(result.system)) { - result.system = [{ type: "text", text }, ...result.system]; + result.system = [...result.system, { type: "text", text }]; } } diff --git a/tests/unit/system-prompt.test.ts b/tests/unit/system-prompt.test.ts index a90ff52bdd..fa53470988 100644 --- a/tests/unit/system-prompt.test.ts +++ b/tests/unit/system-prompt.test.ts @@ -30,7 +30,7 @@ test("injectSystemPrompt: adds system message when none exists", () => { assert.equal(result.messages.length, 2); }); -test("injectSystemPrompt: prepends to existing system message", () => { +test("injectSystemPrompt: appends after existing system message (#2468)", () => { setSystemPromptConfig({ enabled: true, prompt: "GLOBAL:" }); const body = { messages: [ @@ -39,23 +39,24 @@ test("injectSystemPrompt: prepends to existing system message", () => { ], }; const result = injectSystemPrompt(body); - assert.ok(result.messages[0].content.startsWith("GLOBAL:")); - assert.ok(result.messages[0].content.includes("Original prompt")); + // Global prompt must be the FINAL instruction so it wins over provider/agent blocks. + assert.ok(result.messages[0].content.startsWith("Original prompt")); + assert.ok(result.messages[0].content.trimEnd().endsWith("GLOBAL:")); assert.equal(result.messages.length, 2); }); -test("injectSystemPrompt: Claude body.system field", () => { +test("injectSystemPrompt: Claude body.system field appends global last (#2468)", () => { setSystemPromptConfig({ enabled: true, prompt: "GLOBAL:" }); const body = { system: "Claude prompt", messages: [{ role: "user", content: "hi" }], }; const result = injectSystemPrompt(body); - assert.ok(result.system.startsWith("GLOBAL:")); - assert.ok(result.system.includes("Claude prompt")); + assert.ok(result.system.startsWith("Claude prompt")); + assert.ok(result.system.trimEnd().endsWith("GLOBAL:")); }); -test("injectSystemPrompt: Claude array system field", () => { +test("injectSystemPrompt: Claude array system field appends global last (#2468)", () => { setSystemPromptConfig({ enabled: true, prompt: "GLOBAL:" }); const body = { system: [{ type: "text", text: "Claude prompt" }], @@ -63,7 +64,8 @@ test("injectSystemPrompt: Claude array system field", () => { }; const result = injectSystemPrompt(body); assert.ok(Array.isArray(result.system)); - assert.equal(result.system[0].text, "GLOBAL:"); + assert.equal(result.system[0].text, "Claude prompt"); + assert.equal(result.system[result.system.length - 1].text, "GLOBAL:"); assert.equal(result.system.length, 2); }); From f47531bd471eddae0b97212025c3eb9bffb8813e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:53:30 -0300 Subject: [PATCH 014/112] fix(settings): hydrate Global System Prompt on startup and after import (#2470) --- src/app/api/db-backups/import/route.ts | 13 +++++++++++++ src/app/api/settings/import-json/route.ts | 9 +++++++++ src/server-init.ts | 9 +++++++++ 3 files changed, 31 insertions(+) diff --git a/src/app/api/db-backups/import/route.ts b/src/app/api/db-backups/import/route.ts index 645a5b90b9..2afcfccce6 100644 --- a/src/app/api/db-backups/import/route.ts +++ b/src/app/api/db-backups/import/route.ts @@ -6,6 +6,8 @@ import os from "os"; import { getDbInstance, resetDbInstance, SQLITE_FILE } from "@/lib/db/core"; import { backupDbFile } from "@/lib/db/backup"; import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; +import { getSettings } from "@/lib/db/settings"; +import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts"; const MAX_UPLOAD_SIZE = 100 * 1024 * 1024; // 100 MB @@ -154,6 +156,17 @@ export async function POST(request: Request) { `[DB] Imported database from upload: ${connCount} connections, ${nodeCount} nodes, ${comboCount} combos, ${keyCount} API keys` ); + // The DB was replaced wholesale — re-hydrate the in-memory Global System Prompt so it + // reflects the imported settings without requiring a restart (#2470). + try { + const importedSettings = getSettings(); + if (importedSettings.systemPrompt) { + setSystemPromptConfig(importedSettings.systemPrompt); + } + } catch { + // non-fatal: import succeeded; system prompt will hydrate on next restart + } + return NextResponse.json({ imported: true, filename: fileName, diff --git a/src/app/api/settings/import-json/route.ts b/src/app/api/settings/import-json/route.ts index beecf58cf3..fe1b0688d6 100644 --- a/src/app/api/settings/import-json/route.ts +++ b/src/app/api/settings/import-json/route.ts @@ -3,6 +3,8 @@ import { getDbInstance } from "@/lib/db/core"; import { backupDbFile } from "@/lib/db/backup"; import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; import { runJsonMigration, type LegacyJsonData } from "@/lib/db/jsonMigration"; +import { getSettings } from "@/lib/db/settings"; +import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts"; /** * POST /api/settings/import-json @@ -65,6 +67,13 @@ export async function POST(request: Request) { // Delegate the actual migration to the shared helper (avoids duplication with core.ts) const counts = runJsonMigration(db, data); + // Re-hydrate the in-memory Global System Prompt config — the migration writes it to + // the DB but the in-memory state would stay stale until a restart otherwise (#2470). + const importedSettings = getSettings(); + if (importedSettings.systemPrompt) { + setSystemPromptConfig(importedSettings.systemPrompt); + } + console.log( `[JSON Import] Imported ${counts.connections} connections, ${counts.nodes} nodes, ` + `${counts.combos} combos, ${counts.apiKeys} API keys, ` + diff --git a/src/server-init.ts b/src/server-init.ts index 9a94afaf61..829fd0663f 100644 --- a/src/server-init.ts +++ b/src/server-init.ts @@ -8,6 +8,7 @@ import { startBudgetResetJob } from "./lib/jobs/budgetResetJob"; import { startReasoningCacheCleanupJob } from "./lib/jobs/reasoningCacheCleanupJob"; import { getSettings } from "./lib/db/settings"; import { applyRuntimeSettings } from "./lib/config/runtimeSettings"; +import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts"; import { startRuntimeConfigHotReload } from "./lib/config/hotReload"; import { startSpendBatchWriter } from "./lib/spend/batchWriter"; import { registerDefaultGuardrails } from "./lib/guardrails"; @@ -76,6 +77,14 @@ async function startServer() { ); } + // Restore the Global System Prompt into the in-memory config. It lives in the + // `settings.systemPrompt` key but is NOT covered by applyRuntimeSettings, so without + // this the toggle/prompt revert to defaults on every restart (#2470). + if (settings.systemPrompt) { + setSystemPromptConfig(settings.systemPrompt); + startupLog.info("Global System Prompt restored from settings"); + } + // Initialize cloud sync startSpendBatchWriter(); registerDefaultGuardrails(); From 69a80d3ee4e124b8034d147f3169b3fac0407e52 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:54:23 -0300 Subject: [PATCH 015/112] fix(kiro): refresh imported social tokens via social-auth, not AWS OIDC (#2467) --- open-sse/services/tokenRefresh.ts | 7 ++-- src/lib/oauth/services/kiro.ts | 6 ++-- tests/unit/token-refresh-service.test.ts | 41 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index d22173ec5d..94001479ce 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -611,8 +611,11 @@ export async function refreshKiroToken( const region = providerSpecificData?.region; // AWS SSO OIDC (Builder ID or IDC) - // If clientId and clientSecret exist, assume AWS SSO OIDC (default to builder-id if authMethod not specified) - if (clientId && clientSecret) { + // If clientId and clientSecret exist, assume AWS SSO OIDC (default to builder-id if authMethod not specified). + // Exception: imported social tokens (authMethod === "imported") carry a freshly-registered + // clientId/clientSecret but their refresh token is Kiro-social-issued — the isolated OIDC client + // cannot refresh it, so they must fall through to the social auth path (#2467). + if (clientId && clientSecret && authMethod !== "imported") { const endpoint = `https://oidc.${region || "us-east-1"}.amazonaws.com/token`; const response = await runWithProxyContext(proxyConfig, () => diff --git a/src/lib/oauth/services/kiro.ts b/src/lib/oauth/services/kiro.ts index a399ce7f18..a335d0d1cd 100644 --- a/src/lib/oauth/services/kiro.ts +++ b/src/lib/oauth/services/kiro.ts @@ -184,8 +184,10 @@ export class KiroService { async refreshToken(refreshToken: string, providerSpecificData: any = {}) { const { authMethod, clientId, clientSecret, region } = providerSpecificData; - // AWS SSO OIDC refresh (Builder ID or IDC) - if (clientId && clientSecret) { + // AWS SSO OIDC refresh (Builder ID or IDC). + // Imported social tokens (authMethod === "imported") have a registered clientId/clientSecret + // but a Kiro-social refresh token the OIDC client can't refresh — use the social path (#2467). + if (clientId && clientSecret && authMethod !== "imported") { const endpoint = `https://oidc.${region || "us-east-1"}.amazonaws.com/token`; const response = await fetch(endpoint, { diff --git a/tests/unit/token-refresh-service.test.ts b/tests/unit/token-refresh-service.test.ts index 4727684bdb..143ef92a8a 100644 --- a/tests/unit/token-refresh-service.test.ts +++ b/tests/unit/token-refresh-service.test.ts @@ -589,6 +589,47 @@ test("refreshKiroToken uses AWS OIDC path for social-auth token when clientId is }); }); +// Issue #2467 — an IMPORTED social token (authMethod === "imported") carries a +// freshly-registered clientId/clientSecret, but its refresh token is Kiro-social-issued +// and the isolated OIDC client cannot refresh it. It must use the social-auth endpoint, +// NOT AWS OIDC (which is what #2328 enabled for authMethod "google"). +test("refreshKiroToken uses social-auth path for imported token even with clientId (#2467)", async () => { + const log = createLog(); + const calls: any[] = []; + + await withMockedFetch( + async (url, options = {}) => { + calls.push({ url, options }); + return jsonResponse({ + accessToken: "kiro-imported-access", + refreshToken: "kiro-imported-refresh-next", + expiresIn: 1100, + }); + }, + async () => { + const result = await refreshKiroToken( + "kiro-imported-refresh", + { + authMethod: "imported", + clientId: "isolated-client-id", + clientSecret: "isolated-client-secret", + region: "us-east-1", + }, + log + ); + assert.equal(result.accessToken, "kiro-imported-access"); + } + ); + + // Must call the shared social-auth tokenUrl — NOT the AWS OIDC endpoint. + assert.equal( + calls[0].url, + PROVIDERS.kiro.tokenUrl, + `expected social-auth endpoint but got ${calls[0].url}` + ); + assert.ok(!calls[0].url.includes("oidc."), "imported token must not use AWS OIDC"); +}); + test("refreshQoderToken uses basic auth once qoder oauth settings are configured", async () => { const log = createLog(); const calls: any[] = []; From d01b1bd7621c19353d3e68a67a52d4a93f39de4b Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:55:20 -0300 Subject: [PATCH 016/112] fix(antigravity): resolve projectId from providerSpecificData fallback (#2480) --- open-sse/executors/antigravity.ts | 3 +++ .../translator/request/openai-to-gemini.ts | 10 +++++++- .../unit/translator-openai-to-gemini.test.ts | 23 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index f08ce7e576..28cba9ec38 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -583,6 +583,9 @@ export class AntigravityExecutor extends BaseExecutor { : credentials.refreshToken, expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : undefined, projectId: credentials.projectId, + // Preserve providerSpecificData so a projectId stored there survives the refresh + // (the onCredentialsRefreshed DB write) instead of being dropped → 422 (#2480). + providerSpecificData: credentials.providerSpecificData, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 2140278d3f..cc8c744a11 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -508,7 +508,15 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra // Both Antigravity and Gemini CLI need the project field for the Cloud Code API. // For Gemini CLI, the stored projectId may be stale; the executor's transformRequest // refreshes it via loadCodeAssist before the request is sent to the API. - let projectId = credentials?.projectId; + // Fall back to providerSpecificData.projectId — some connections (and post-refresh + // credentials) store it there rather than at the top level, which otherwise produced a + // spurious 422 "Missing Google projectId" on the Antigravity /v1beta path (#2480). + const providerSpecificProjectId = ( + credentials?.providerSpecificData as { projectId?: unknown } | undefined + )?.projectId; + let projectId = + credentials?.projectId || + (typeof providerSpecificProjectId === "string" ? providerSpecificProjectId : ""); if (!projectId) { console.warn( diff --git a/tests/unit/translator-openai-to-gemini.test.ts b/tests/unit/translator-openai-to-gemini.test.ts index daf2b12e61..993be797c1 100644 --- a/tests/unit/translator-openai-to-gemini.test.ts +++ b/tests/unit/translator-openai-to-gemini.test.ts @@ -891,3 +891,26 @@ test("OpenAI -> Antigravity Gemini path preserves thinkingConfig (only Claude is assert.equal((result as any).request?.generationConfig.thinkingConfig.thinkingBudget > 0, true); assert.equal((result as any).request?.generationConfig.thinkingConfig.includeThoughts, true); }); + +// Regression for #2480: when projectId is stored in providerSpecificData rather than at +// the top level of the credential record, the Antigravity Cloud Code envelope must still +// pick it up — otherwise the /v1beta path 422s with "Missing Google projectId". +test("openaiToAntigravityRequest falls back to providerSpecificData.projectId (#2480)", () => { + const result = openaiToAntigravityRequest( + "gemini-3.1-flash-lite", + { messages: [{ role: "user", content: "Hello" }] }, + false, + { providerSpecificData: { projectId: "proj-from-psd" } } as any + ); + assert.equal(result.project, "proj-from-psd"); +}); + +test("openaiToAntigravityRequest prefers top-level projectId over providerSpecificData (#2480)", () => { + const result = openaiToAntigravityRequest( + "gemini-3.1-flash-lite", + { messages: [{ role: "user", content: "Hello" }] }, + false, + { projectId: "proj-top", providerSpecificData: { projectId: "proj-psd" } } as any + ); + assert.equal(result.project, "proj-top"); +}); From 1d6077f8cfec8c34f314f99ecdaa89ed3b4b0cb0 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:56:15 -0300 Subject: [PATCH 017/112] fix(api): /v1beta/models lists only active-connection providers (#2483) --- src/app/api/v1beta/models/route.ts | 37 ++++++++++++++++++++++++-- tests/unit/v1beta-models-route.test.ts | 33 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/app/api/v1beta/models/route.ts b/src/app/api/v1beta/models/route.ts index 8d6a50834b..7e782d6e8e 100644 --- a/src/app/api/v1beta/models/route.ts +++ b/src/app/api/v1beta/models/route.ts @@ -1,13 +1,38 @@ -import { PROVIDER_MODELS } from "@/shared/constants/models"; +import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; import { getAllCustomModels, getAllSyncedAvailableModels, getSyncedAvailableModels, } from "@/lib/db/models"; +import { getProviderConnections } from "@/lib/localDb"; import { getResolvedModelCapabilities } from "@/lib/modelCapabilities"; import { getSyncedCapabilities } from "@/lib/modelsDevSync"; +/** + * Build the set of provider keys (raw id + alias) that have at least one active/validated + * connection. Mirrors the active-provider filter used by the OpenAI-format /v1/models + * catalog so /v1beta/models only lists models the user can actually call (#2483). + */ +async function getActiveProviderKeys(): Promise> { + const keys = new Set(); + try { + const connections = await getProviderConnections(); + for (const conn of connections) { + if (conn.isActive === false) continue; + const provider = conn.provider; + if (!provider) continue; + keys.add(provider); + const alias = (PROVIDER_ID_TO_ALIAS as Record)[provider]; + if (alias) keys.add(alias); + } + } catch (e) { + // DB unavailable — return empty set (safe default: list nothing provider-gated) + console.error("[v1beta/models] Could not fetch provider connections:", e); + } + return keys; +} + /** * Handle CORS preflight */ @@ -29,8 +54,12 @@ export async function GET() { getSyncedCapabilities(); const models = []; + // Only list models whose provider has an active/validated connection (#2483). + const activeKeys = await getActiveProviderKeys(); + // Built-in models (hardcoded defaults) for (const [provider, providerModels] of Object.entries(PROVIDER_MODELS)) { + if (!activeKeys.has(provider)) continue; for (const model of providerModels) { const resolved = getResolvedModelCapabilities({ provider, model: model.id }); models.push({ @@ -56,7 +85,9 @@ export async function GET() { } } try { - const syncedGeminiModels = await getSyncedAvailableModels("gemini"); + const syncedGeminiModels = activeKeys.has("gemini") + ? await getSyncedAvailableModels("gemini") + : []; for (const m of syncedGeminiModels) { models.push({ name: `models/gemini/${m.id}`, @@ -79,6 +110,7 @@ export async function GET() { const syncedModelsMap = await getAllSyncedAvailableModels(); for (const [providerId, syncedModels] of Object.entries(syncedModelsMap)) { if (providerId === "gemini") continue; + if (!activeKeys.has(providerId)) continue; if (!Array.isArray(syncedModels)) continue; for (const m of syncedModels) { if (!m || typeof m.id !== "string") continue; @@ -119,6 +151,7 @@ export async function GET() { if (!Array.isArray(rawModels)) continue; // Skip Gemini — handled by syncedAvailableModels above if (providerId === "gemini") continue; + if (!activeKeys.has(providerId)) continue; for (const model of rawModels) { if (!model || typeof model !== "object" || typeof (model as any).id !== "string") continue; diff --git a/tests/unit/v1beta-models-route.test.ts b/tests/unit/v1beta-models-route.test.ts index 21f6b210b4..1f66e7ae24 100644 --- a/tests/unit/v1beta-models-route.test.ts +++ b/tests/unit/v1beta-models-route.test.ts @@ -10,8 +10,18 @@ process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "v1beta-models-test-s const core = await import("../../src/lib/db/core.ts"); const modelsDb = await import("../../src/lib/db/models.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); const v1betaModelsRoute = await import("../../src/app/api/v1beta/models/route.ts"); +async function addActiveConnection(provider: string) { + await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `test-key-${provider}`, + testStatus: "active", + }); +} + async function resetStorage() { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -28,6 +38,8 @@ test.after(async () => { }); test("v1beta models route deduplicates custom models against built-in and synced entries", async () => { + // #2483: the route now lists only models whose provider has an active connection. + await addActiveConnection("openai"); await modelsDb.replaceSyncedAvailableModelsForConnection("openai", "conn-main", [ { id: "gpt-4o", @@ -53,3 +65,24 @@ test("v1beta models route deduplicates custom models against built-in and synced assert.equal(names.filter((name) => name === "models/openai/review-sync-only").length, 1); assert.equal(names.filter((name) => name === "models/openai/review-manual-only").length, 1); }); + +test("v1beta models route excludes providers without an active connection (#2483)", async () => { + // No connections configured at all → no built-in catalog models should leak. + const emptyResp = await v1betaModelsRoute.GET(); + const emptyBody = (await emptyResp.json()) as { models: Array<{ name: string }> }; + assert.equal(emptyResp.status, 200); + assert.equal(emptyBody.models.length, 0, "no active connections → empty model list"); + + // Configure ONLY an anthropic connection; custom models for an unconfigured provider + // (kie) must NOT appear, while anthropic catalog models do. + await addActiveConnection("anthropic"); + await modelsDb.addCustomModel("kie", "claude-opus-4-7", "Kie Claude Opus"); + const resp = await v1betaModelsRoute.GET(); + const body = (await resp.json()) as { models: Array<{ name: string }> }; + const names = body.models.map((m) => m.name); + assert.ok(!names.some((n) => n.startsWith("models/kie/")), "unconfigured kie must be excluded"); + assert.ok( + names.some((n) => n.startsWith("models/anthropic/")), + "configured anthropic must be present" + ); +}); From f4534c12e6119410c06e3f4395b89a57a5a4e77e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 10:57:09 -0300 Subject: [PATCH 018/112] docs(changelog): record #2469, #2470, #2468, #2467, #2480, #2483 --- CHANGELOG.md | 6 ++++++ docs/i18n/ar/CHANGELOG.md | 6 ++++++ docs/i18n/az/CHANGELOG.md | 6 ++++++ docs/i18n/bg/CHANGELOG.md | 6 ++++++ docs/i18n/bn/CHANGELOG.md | 6 ++++++ docs/i18n/cs/CHANGELOG.md | 6 ++++++ docs/i18n/da/CHANGELOG.md | 6 ++++++ docs/i18n/de/CHANGELOG.md | 6 ++++++ docs/i18n/es/CHANGELOG.md | 6 ++++++ docs/i18n/fa/CHANGELOG.md | 6 ++++++ docs/i18n/fi/CHANGELOG.md | 6 ++++++ docs/i18n/fr/CHANGELOG.md | 6 ++++++ docs/i18n/gu/CHANGELOG.md | 6 ++++++ docs/i18n/he/CHANGELOG.md | 6 ++++++ docs/i18n/hi/CHANGELOG.md | 6 ++++++ docs/i18n/hu/CHANGELOG.md | 6 ++++++ docs/i18n/id/CHANGELOG.md | 6 ++++++ docs/i18n/in/CHANGELOG.md | 6 ++++++ docs/i18n/it/CHANGELOG.md | 6 ++++++ docs/i18n/ja/CHANGELOG.md | 6 ++++++ docs/i18n/ko/CHANGELOG.md | 6 ++++++ docs/i18n/mr/CHANGELOG.md | 6 ++++++ docs/i18n/ms/CHANGELOG.md | 6 ++++++ docs/i18n/nl/CHANGELOG.md | 6 ++++++ docs/i18n/no/CHANGELOG.md | 6 ++++++ docs/i18n/phi/CHANGELOG.md | 6 ++++++ docs/i18n/pl/CHANGELOG.md | 6 ++++++ docs/i18n/pt-BR/CHANGELOG.md | 6 ++++++ docs/i18n/pt/CHANGELOG.md | 6 ++++++ docs/i18n/ro/CHANGELOG.md | 6 ++++++ docs/i18n/ru/CHANGELOG.md | 6 ++++++ docs/i18n/sk/CHANGELOG.md | 6 ++++++ docs/i18n/sv/CHANGELOG.md | 6 ++++++ docs/i18n/sw/CHANGELOG.md | 6 ++++++ docs/i18n/ta/CHANGELOG.md | 6 ++++++ docs/i18n/te/CHANGELOG.md | 6 ++++++ docs/i18n/th/CHANGELOG.md | 6 ++++++ docs/i18n/tr/CHANGELOG.md | 6 ++++++ docs/i18n/uk-UA/CHANGELOG.md | 6 ++++++ docs/i18n/ur/CHANGELOG.md | 6 ++++++ docs/i18n/vi/CHANGELOG.md | 6 ++++++ docs/i18n/zh-CN/CHANGELOG.md | 6 ++++++ 42 files changed, 252 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c2ec57457..e5df4e472b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - **fix(claude):** gate the heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was receiving `context-1m` and rejecting it with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Anthropic-native Claude OAuth passthrough (converts them to `redacted_thinking`), fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client (`perplexityTlsClient.ts`) so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation and execution now distinguish a Cloudflare block from an invalid session cookie instead of always blaming the cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions (Kiro, OpenCode, Hermes, …) injected into the system message no longer override the user's global prompt via recency bias. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC — imported tokens carry a registered `clientId`/`clientSecret` but a social-issued refresh token the OIDC client cannot refresh, so auto-refresh was failing with "provider returned no new token". ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId` for connections that store the project there. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, matching the OpenAI-format `/v1/models` behavior, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) --- diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index a0dc4763f7..612866b72b 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index 7ffb64f193..449bda8ac5 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index 59c22fddc3..c4703ceec0 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index b8aa9541ea..2d35ef6e25 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 5cef7dbfb0..26e745b32d 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 3bb7430fe3..71b80bdc5d 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index d1f51f1b3a..34fd0a433a 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index 4967486824..a6a25f470b 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index d0527b9457..ea072a173d 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 3908a5d7be..38c96d965c 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index 5dfdd5f7b3..f2ae83ae3e 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index 09ba8b41e1..0ba6c7573a 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 358cc5c7a9..cb999a3e1d 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index a85a8d8a03..9685ce7223 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index e3bf9fb75d..a4557dbcd1 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index a696808d9e..600ea415b8 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index 91361c5fc7..c6f8360a86 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index ffca6711c3..d8217401c6 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 0a0cd46212..e410873231 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index e2033281a9..ef8775a3ba 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index c280e4480f..d7f59651d9 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 5ad912fdc3..75b046a2ba 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index 5198a6435d..340bc6886b 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 42de817607..7ef1541049 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 85c171d598..1f3a97539d 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index daf5037833..40e593cc5e 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 954fdbe97d..bf68acb3d7 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 696f7b1ac7..6ccd4ede1d 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index a8a96cac5c..c81a7869ef 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index d0e037b1ab..c3cd4de551 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index 90560ef3cb..81430c3284 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index 6598a6f5e8..5dd70b427f 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 6a12e325a0..ac3ce6fd41 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index c0047505c9..d62c47cea0 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index a424891cd8..a1cc635d38 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index e5b4f79fd7..69dc017ce0 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index 38918ac85a..23d1a76060 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index 96385ae654..4282c71e3b 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 7af521283d..aa1f227591 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 702717f69e..ff3f142417 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 0aa68addb4..3b1338893f 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -13,6 +13,12 @@ - **fix(claude):** gate heavy-agent beta headers (`context-1m-2025-08-07`, `effort-2025-11-24`, `advanced-tool-use-2025-11-20`) on Opus/Sonnet only and stop deleting Haiku's `thinking` config — Haiku with OAuth was rejecting `context-1m` with `[400]: This authentication style is incompatible with the long context beta header`. Also sanitizes historical `thinking` block signatures in Claude OAuth passthrough, fixing `[400]: Invalid signature in thinking block` on mid-session model switches. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge — mirrors the existing `chatgpt-web` approach. Validation/execution now distinguish a Cloudflare block from an invalid session cookie. New env vars `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` / `OMNIROUTE_PPLX_TLS_GRACE_MS`. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path — a corrupted or mis-typed credential could throw `TypeError: ... is not a function` mid-validation instead of returning a clean result. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — thanks @disonjer) +- **fix(settings):** restore the Global System Prompt into the in-memory config on server startup and after JSON/SQLite import — it was only loaded by the PUT endpoint, so the toggle/prompt silently reverted to defaults after any restart or import. ([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — thanks @disonjer) +- **fix(settings):** append the Global System Prompt **after** existing system content instead of prepending it, so provider/agent instructions injected into the system message no longer override the user's global prompt. ([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — thanks @disonjer) +- **fix(kiro):** refresh imported social tokens (`authMethod === "imported"`) via the Kiro social-auth endpoint instead of AWS SSO OIDC. ([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — thanks @disonjer) +- **fix(antigravity):** resolve the Cloud Code `projectId` from `providerSpecificData` as a fallback (and preserve it across token refresh) so the Gemini `/v1beta` streaming path stops returning a spurious `422 Missing Google projectId`. ([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) +- **fix(api):** `GET /v1beta/models` now lists only models whose provider has an active/validated connection, instead of returning the entire catalog. ([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) ## [3.8.1] — 2026-05-20 From 852acc3632ebe8f1f090513c6881e9f7d940e4b5 Mon Sep 17 00:00:00 2001 From: ivan_yakimkin Date: Thu, 21 May 2026 17:24:04 +0300 Subject: [PATCH 019/112] fix(antigravity): align subscription tier detection with Antigravity Manager Extract paid/current/restricted tiers from loadCodeAssist (shared module), fix invalid LINUX metadata on Docker, refresh tier on quota update without re-auth, and persist tier fields back to connections. Co-authored-by: Cursor --- open-sse/services/antigravityHeaders.ts | 27 +-- open-sse/services/codeAssistSubscription.ts | 93 +++++++ open-sse/services/usage.ts | 227 ++++++++++-------- .../usage/components/ProviderLimits/utils.tsx | 13 +- src/lib/oauth/providers/antigravity.ts | 16 +- src/lib/oauth/services/antigravity.ts | 12 +- src/lib/usage/providerLimits.ts | 44 ++++ tests/unit/antigravity-headers.test.ts | 9 + tests/unit/code-assist-subscription.test.ts | 62 +++++ tests/unit/usage-service-hardening.test.ts | 17 ++ 10 files changed, 370 insertions(+), 150 deletions(-) create mode 100644 open-sse/services/codeAssistSubscription.ts create mode 100644 tests/unit/antigravity-headers.test.ts create mode 100644 tests/unit/code-assist-subscription.test.ts diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts index 3648d56912..b0829ad38c 100644 --- a/open-sse/services/antigravityHeaders.ts +++ b/open-sse/services/antigravityHeaders.ts @@ -25,20 +25,6 @@ export const ANTIGRAVITY_NODE_API_CLIENT = "google-api-nodejs-client/10.3.0"; // Harness/bootstrap X-Goog-Api-Client synced with CLIProxyAPI misc.AntigravityGoogAPIClientUA. export const ANTIGRAVITY_CREDIT_PROBE_API_CLIENT = "gl-node/22.21.1"; export const ANTIGRAVITY_API_CLIENT = ANTIGRAVITY_CREDIT_PROBE_API_CLIENT; -type AntigravityLoadCodeAssistPlatform = "MACOS" | "WINDOWS" | "LINUX"; - -function getAntigravityLoadCodeAssistPlatformLabel( - platform: NodeJS.Platform = process.platform -): AntigravityLoadCodeAssistPlatform { - switch (platform) { - case "darwin": - return "MACOS"; - case "win32": - return "WINDOWS"; - default: - return "LINUX"; - } -} function withOptionalBearerAuth( headers: Record, @@ -84,20 +70,15 @@ export function antigravityNativeOAuthUserAgent(): string { return `vscode/1.X.X (Antigravity/${getCachedAntigravityVersion()})`; } -export function getAntigravityLoadCodeAssistMetadata( - platform: NodeJS.Platform = process.platform -): Record { +/** Matches Antigravity-Manager quota.rs: only ideType (no platform — LINUX is rejected). */ +export function getAntigravityLoadCodeAssistMetadata(): Record { return { ideType: "ANTIGRAVITY", - platform: getAntigravityLoadCodeAssistPlatformLabel(platform), - pluginType: "GEMINI", }; } -export function getAntigravityLoadCodeAssistClientMetadata( - platform: NodeJS.Platform = process.platform -): string { - return JSON.stringify(getAntigravityLoadCodeAssistMetadata(platform)); +export function getAntigravityLoadCodeAssistClientMetadata(): string { + return JSON.stringify(getAntigravityLoadCodeAssistMetadata()); } export function getAntigravityHeaders( diff --git a/open-sse/services/codeAssistSubscription.ts b/open-sse/services/codeAssistSubscription.ts new file mode 100644 index 0000000000..f619d478c7 --- /dev/null +++ b/open-sse/services/codeAssistSubscription.ts @@ -0,0 +1,93 @@ +/** + * Code Assist (loadCodeAssist) subscription tier extraction. + * Mirrors Antigravity-Manager src-tauri/src/modules/quota.rs fetch_project_id(). + */ + +type JsonRecord = Record; + +function toRecord(value: unknown): JsonRecord { + return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; +} + +function pickTierField(tier: unknown, field: "name" | "id"): string | null { + const record = toRecord(tier); + const value = record[field]; + return typeof value === "string" && value.trim() ? value.trim() : null; +} + +function isIneligible(subscription: JsonRecord): boolean { + const ineligible = subscription.ineligibleTiers; + return Array.isArray(ineligible) && ineligible.length > 0; +} + +function findDefaultAllowedTier(subscription: JsonRecord): JsonRecord | null { + if (!Array.isArray(subscription.allowedTiers)) return null; + for (const tierValue of subscription.allowedTiers) { + const tier = toRecord(tierValue); + if (tier.isDefault) return tier; + } + return null; +} + +/** + * Display subscription tier from loadCodeAssist (paid → current → restricted default). + */ +export function extractCodeAssistSubscriptionTier(subscriptionInfo: unknown): string | null { + const subscription = toRecord(subscriptionInfo); + if (Object.keys(subscription).length === 0) return null; + + let tier = + pickTierField(subscription.paidTier, "name") || pickTierField(subscription.paidTier, "id"); + + if (!tier) { + if (!isIneligible(subscription)) { + tier = + pickTierField(subscription.currentTier, "name") || + pickTierField(subscription.currentTier, "id"); + } else { + const defaultTier = findDefaultAllowedTier(subscription); + if (defaultTier) { + const name = pickTierField(defaultTier, "name"); + const id = pickTierField(defaultTier, "id"); + if (name) tier = `${name} (Restricted)`; + else if (id) tier = `${id} (Restricted)`; + } + } + } + + return tier; +} + +/** + * Tier ID for onboardUser tier_id (paid → current → restricted default → legacy-tier). + */ +export function extractCodeAssistOnboardTierId(subscriptionInfo: unknown): string { + const subscription = toRecord(subscriptionInfo); + + const paidId = pickTierField(subscription.paidTier, "id"); + if (paidId) return paidId; + + if (!isIneligible(subscription)) { + const currentId = pickTierField(subscription.currentTier, "id"); + if (currentId) return currentId; + } else { + const defaultTier = findDefaultAllowedTier(subscription); + const defaultId = defaultTier ? pickTierField(defaultTier, "id") : null; + if (defaultId) return defaultId; + } + + if (Array.isArray(subscription.allowedTiers)) { + for (const tierValue of subscription.allowedTiers) { + const tier = toRecord(tierValue); + if (tier.isDefault) { + const id = pickTierField(tier, "id"); + if (id) return id; + } + } + } + + const currentId = pickTierField(subscription.currentTier, "id"); + if (currentId) return currentId; + + return "legacy-tier"; +} diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 4f2c7a9c62..10a24c33f9 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -38,6 +38,10 @@ import { import { getCreditsMode } from "./antigravityCredits.ts"; import { CLAUDE_CODE_VERSION, fetchClaudeBootstrap } from "../executors/claudeIdentity.ts"; import { generateAntigravityRequestId, getAntigravitySessionId } from "./antigravityIdentity.ts"; +import { + extractCodeAssistOnboardTierId, + extractCodeAssistSubscriptionTier, +} from "./codeAssistSubscription.ts"; // Antigravity API config (credentials from PROVIDERS via credential loader) const ANTIGRAVITY_CONFIG = { @@ -1467,54 +1471,7 @@ async function getGeminiCliSubscriptionInfo(accessToken: string): Promise { const profile = getAntigravityClientProfile({ providerSpecificData }); const cacheKey = `${accessToken.substring(0, 16)}:${profile}`; - const cached = _antigravitySubCache.get(cacheKey); - if (cached && Date.now() - cached.fetchedAt < ANTIGRAVITY_CACHE_TTL_MS) { - return cached.data; + if (options.forceRefresh) { + _antigravitySubCache.delete(cacheKey); + } else { + const cached = _antigravitySubCache.get(cacheKey); + if (cached && Date.now() - cached.fetchedAt < ANTIGRAVITY_CACHE_TTL_MS) { + return cached.data; + } } const data = await getAntigravitySubscriptionInfo(accessToken, providerSpecificData); - _antigravitySubCache.set(cacheKey, { data, fetchedAt: Date.now() }); + if (data != null) { + _antigravitySubCache.set(cacheKey, { data, fetchedAt: Date.now() }); + } return data; } @@ -2551,4 +2568,6 @@ export const __testing = { inferGitHubPlanName, getGeminiCliPlanLabel, getAntigravityPlanLabel, + extractCodeAssistSubscriptionTier, + extractCodeAssistOnboardTierId, }; diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index 6851c621e3..95297ee2a4 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -404,10 +404,11 @@ export function parseQuotaData(provider, data) { */ export function resolvePlanValue(plan, providerSpecificData) { const psd = toRecord(providerSpecificData); - const candidates = [ - plan, + const livePlan = normalizePlanCandidate(plan); + const persistedCandidates = [ psd.workspacePlanType, psd.plan, + psd.subscriptionTier, psd.subscription, psd.tier, psd.accountTier, @@ -416,12 +417,16 @@ export function resolvePlanValue(plan, providerSpecificData) { psd.organizationType, ]; - for (const candidate of candidates) { + if (livePlan && normalizePlanTier(livePlan).key !== "free") { + return livePlan; + } + + for (const candidate of persistedCandidates) { const normalized = normalizePlanCandidate(candidate); if (normalized) return normalized; } - return null; + return livePlan || null; } /** diff --git a/src/lib/oauth/providers/antigravity.ts b/src/lib/oauth/providers/antigravity.ts index 566e4ddd5a..06ef9ea510 100644 --- a/src/lib/oauth/providers/antigravity.ts +++ b/src/lib/oauth/providers/antigravity.ts @@ -4,6 +4,7 @@ import { getAntigravityHeaders, getAntigravityLoadCodeAssistMetadata, } from "@omniroute/open-sse/services/antigravityHeaders.ts"; +import { extractCodeAssistOnboardTierId } from "@omniroute/open-sse/services/codeAssistSubscription.ts"; async function fetchFirstOk(endpoints: string[], init: RequestInit) { let lastError: unknown = null; @@ -82,14 +83,7 @@ export const antigravity = { }); const data = await loadRes.json(); projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || ""; - if (Array.isArray(data.allowedTiers)) { - for (const tier of data.allowedTiers) { - if (tier.isDefault && tier.id) { - tierId = tier.id.trim(); - break; - } - } - } + tierId = extractCodeAssistOnboardTierId(data); } catch (e) { console.log("Failed to load code assist:", e); } @@ -118,7 +112,7 @@ export const antigravity = { } } - return { userInfo, projectId }; + return { userInfo, projectId, tierId }; }, mapTokens: (tokens, extra) => ({ accessToken: tokens.access_token, @@ -127,5 +121,9 @@ export const antigravity = { scope: tokens.scope, email: extra?.userInfo?.email, projectId: extra?.projectId, + providerSpecificData: { + projectId: extra?.projectId, + tier: extra?.tierId, + }, }), }; diff --git a/src/lib/oauth/services/antigravity.ts b/src/lib/oauth/services/antigravity.ts index 91d530790e..d3a0ad4362 100644 --- a/src/lib/oauth/services/antigravity.ts +++ b/src/lib/oauth/services/antigravity.ts @@ -6,6 +6,7 @@ import { getAntigravityHeaders, getAntigravityLoadCodeAssistMetadata, } from "@omniroute/open-sse/services/antigravityHeaders.ts"; +import { extractCodeAssistOnboardTierId } from "@omniroute/open-sse/services/codeAssistSubscription.ts"; import { getServerCredentials } from "../config/index"; import { startLocalServer } from "../utils/server"; import { spinner as createSpinner } from "../utils/ui"; @@ -145,16 +146,7 @@ export class AntigravityService { projectId = projectId.id; } - // Extract tier ID (default to legacy-tier) - let tierId = "legacy-tier"; - if (Array.isArray(data.allowedTiers)) { - for (const tier of data.allowedTiers) { - if (tier.isDefault && tier.id) { - tierId = tier.id.trim(); - break; - } - } - } + const tierId = extractCodeAssistOnboardTierId(data); return { projectId, tierId, raw: data }; } diff --git a/src/lib/usage/providerLimits.ts b/src/lib/usage/providerLimits.ts index 26d83fff16..0d13b0a38e 100644 --- a/src/lib/usage/providerLimits.ts +++ b/src/lib/usage/providerLimits.ts @@ -18,6 +18,10 @@ import { getMachineId } from "@/shared/utils/machine"; import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers"; import { getExecutor } from "@omniroute/open-sse/executors/index.ts"; import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts"; +import { + extractCodeAssistOnboardTierId, + extractCodeAssistSubscriptionTier, +} from "@omniroute/open-sse/services/codeAssistSubscription.ts"; import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts"; type JsonRecord = Record; @@ -220,6 +224,44 @@ async function syncClaudeExtraUsageStateIfNeeded( }; } +/** Persist Antigravity tier from live loadCodeAssist on quota refresh (not only OAuth). */ +async function syncAntigravitySubscriptionIfNeeded( + connection: ProviderConnectionLike, + usage: JsonRecord +): Promise { + if (connection.provider !== "antigravity") return connection; + + const subscriptionInfo = usage.subscriptionInfo; + if (!subscriptionInfo) return connection; + + const psd = (connection.providerSpecificData || {}) as JsonRecord; + const nextPsd: JsonRecord = { ...psd }; + let changed = false; + + const tierId = extractCodeAssistOnboardTierId(subscriptionInfo); + if (tierId && tierId !== "legacy-tier" && psd.tier !== tierId) { + nextPsd.tier = tierId; + changed = true; + } + + const subscriptionTier = extractCodeAssistSubscriptionTier(subscriptionInfo); + if (subscriptionTier && psd.subscriptionTier !== subscriptionTier) { + nextPsd.subscriptionTier = subscriptionTier; + changed = true; + } + + const plan = typeof usage.plan === "string" ? usage.plan.trim() : ""; + if (plan && psd.plan !== plan) { + nextPsd.plan = plan; + changed = true; + } + + if (!changed) return connection; + + await updateProviderConnection(connection.id, { providerSpecificData: nextPsd }); + return { ...connection, providerSpecificData: nextPsd }; +} + /** Persist refreshed Claude bootstrap fields into psd; writes only on diff. */ async function syncClaudeBootstrapIfNeeded( connection: ProviderConnectionLike, @@ -317,6 +359,7 @@ async function fetchLiveProviderLimitsWithOptions( await syncExpiredStatusIfNeeded(connection, usage); connection = await syncClaudeExtraUsageStateIfNeeded(connection, usage); connection = await syncClaudeBootstrapIfNeeded(connection, usage); + connection = await syncAntigravitySubscriptionIfNeeded(connection, usage); return { connection, usage }; } @@ -377,6 +420,7 @@ async function fetchLiveProviderLimitsWithOptions( await syncExpiredStatusIfNeeded(connection, result.usage); connection = await syncClaudeExtraUsageStateIfNeeded(connection, result.usage); connection = await syncClaudeBootstrapIfNeeded(connection, result.usage); + connection = await syncAntigravitySubscriptionIfNeeded(connection, result.usage); return { connection, diff --git a/tests/unit/antigravity-headers.test.ts b/tests/unit/antigravity-headers.test.ts new file mode 100644 index 0000000000..b65833d648 --- /dev/null +++ b/tests/unit/antigravity-headers.test.ts @@ -0,0 +1,9 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { getAntigravityLoadCodeAssistMetadata } from "../../open-sse/services/antigravityHeaders.ts"; + +test("loadCodeAssist metadata matches Antigravity Manager (ideType only)", () => { + assert.deepEqual(getAntigravityLoadCodeAssistMetadata(), { ideType: "ANTIGRAVITY" }); + assert.equal("platform" in getAntigravityLoadCodeAssistMetadata(), false); + assert.equal("pluginType" in getAntigravityLoadCodeAssistMetadata(), false); +}); diff --git a/tests/unit/code-assist-subscription.test.ts b/tests/unit/code-assist-subscription.test.ts new file mode 100644 index 0000000000..0d8200d039 --- /dev/null +++ b/tests/unit/code-assist-subscription.test.ts @@ -0,0 +1,62 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + extractCodeAssistOnboardTierId, + extractCodeAssistSubscriptionTier, +} from "../../open-sse/services/codeAssistSubscription.ts"; + +test("extractCodeAssistSubscriptionTier prefers paidTier over currentTier", () => { + assert.equal( + extractCodeAssistSubscriptionTier({ + paidTier: { id: "tier_google_one_ai_pro", name: "Google One AI Premium" }, + currentTier: { id: "free-tier", name: "Free" }, + }), + "Google One AI Premium" + ); +}); + +test("extractCodeAssistSubscriptionTier uses currentTier when not ineligible", () => { + assert.equal( + extractCodeAssistSubscriptionTier({ + currentTier: { id: "tier_pro", name: "Pro" }, + allowedTiers: [{ id: "free-tier", isDefault: true }], + }), + "Pro" + ); +}); + +test("extractCodeAssistSubscriptionTier uses restricted default when ineligible", () => { + assert.equal( + extractCodeAssistSubscriptionTier({ + ineligibleTiers: [{ reasonCode: "POLICY" }], + currentTier: { id: "tier_ultra", name: "Ultra" }, + allowedTiers: [{ id: "tier_pro", name: "Pro", isDefault: true }], + }), + "Pro (Restricted)" + ); +}); + +test("extractCodeAssistOnboardTierId prefers paidTier id for onboarding", () => { + assert.equal( + extractCodeAssistOnboardTierId({ + paidTier: { id: "tier_google_one_ai_pro" }, + currentTier: { id: "free-tier" }, + }), + "tier_google_one_ai_pro" + ); +}); + +test("extractCodeAssistOnboardTierId skips currentTier when ineligible", () => { + assert.equal( + extractCodeAssistOnboardTierId({ + ineligibleTiers: [{}], + currentTier: { id: "tier_ultra" }, + allowedTiers: [{ id: "tier_pro", isDefault: true }], + }), + "tier_pro" + ); +}); + +test("extractCodeAssistOnboardTierId falls back to legacy-tier", () => { + assert.equal(extractCodeAssistOnboardTierId({}), "legacy-tier"); +}); diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index 4e0cfccd65..fafbde5af8 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -414,10 +414,12 @@ test("usage service manual Antigravity refresh bypasses usage TTL caches", async process.env.ANTIGRAVITY_CREDITS = "retry"; let probeCalls = 0; let modelCalls = 0; + let loadCodeAssistCalls = 0; globalThis.fetch = async (url) => { const urlStr = String(url); if (urlStr.includes("loadCodeAssist")) { + loadCodeAssistCalls++; return new Response(JSON.stringify({ cloudaicompanionProject: "ag-project" }), { status: 200, }); @@ -460,6 +462,7 @@ test("usage service manual Antigravity refresh bypasses usage TTL caches", async assert.equal(probeCalls, 2); assert.equal(modelCalls, 2); + assert.equal(loadCodeAssistCalls, 2); }); test("usage service handles missing Antigravity access tokens without probing upstream", async () => { @@ -1258,6 +1261,20 @@ test("usage helper branches cover Gemini CLI and Antigravity plan label fallback ); assert.equal(__testing.getAntigravityPlanLabel(null), "Free"); + assert.equal( + __testing.getAntigravityPlanLabel({ + paidTier: { name: "Google One AI Premium" }, + currentTier: { id: "free-tier" }, + }), + "Pro" + ); + assert.equal( + __testing.getAntigravityPlanLabel({ + currentTier: { id: "tier_google_one_ai_pro" }, + allowedTiers: [{ id: "free-tier", isDefault: true }], + }), + "Pro" + ); assert.equal( __testing.getAntigravityPlanLabel({ allowedTiers: [{ id: "tier_pro", isDefault: true }], From 636d4c44fc8c3f3637c42b583829e41b4ab7aeb9 Mon Sep 17 00:00:00 2001 From: ivan_yakimkin Date: Thu, 21 May 2026 17:39:20 +0300 Subject: [PATCH 020/112] refactor(antigravity): address PR review on tier extraction and usage cache Simplify onboard tier ID fallback and reuse subscription lookup in error path. Co-authored-by: Cursor --- open-sse/services/codeAssistSubscription.ts | 16 +++------------- open-sse/services/usage.ts | 14 ++------------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/open-sse/services/codeAssistSubscription.ts b/open-sse/services/codeAssistSubscription.ts index f619d478c7..9662050155 100644 --- a/open-sse/services/codeAssistSubscription.ts +++ b/open-sse/services/codeAssistSubscription.ts @@ -70,21 +70,11 @@ export function extractCodeAssistOnboardTierId(subscriptionInfo: unknown): strin if (!isIneligible(subscription)) { const currentId = pickTierField(subscription.currentTier, "id"); if (currentId) return currentId; - } else { - const defaultTier = findDefaultAllowedTier(subscription); - const defaultId = defaultTier ? pickTierField(defaultTier, "id") : null; - if (defaultId) return defaultId; } - if (Array.isArray(subscription.allowedTiers)) { - for (const tierValue of subscription.allowedTiers) { - const tier = toRecord(tierValue); - if (tier.isDefault) { - const id = pickTierField(tier, "id"); - if (id) return id; - } - } - } + const defaultTier = findDefaultAllowedTier(subscription); + const defaultId = defaultTier ? pickTierField(defaultTier, "id") : null; + if (defaultId) return defaultId; const currentId = pickTierField(subscription.currentTier, "id"); if (currentId) return currentId; diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 10a24c33f9..0839e0c4d1 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -1809,8 +1809,9 @@ async function getAntigravityUsage( return { plan: "Free", message: "Antigravity access token not available." }; } + let subscriptionInfo: unknown = null; try { - const subscriptionInfo = await getAntigravitySubscriptionInfoCached( + subscriptionInfo = await getAntigravitySubscriptionInfoCached( accessToken, providerSpecificData, options @@ -1908,17 +1909,6 @@ async function getAntigravityUsage( subscriptionInfo, }; } catch (error) { - let subscriptionInfo: unknown = null; - try { - subscriptionInfo = await getAntigravitySubscriptionInfoCached( - accessToken, - providerSpecificData, - options - ); - } catch { - subscriptionInfo = null; - } - return { plan: getAntigravityPlanLabel(subscriptionInfo, providerSpecificData), subscriptionInfo, From 953a7540578fe37d4c408815dbd6f1f875c55512 Mon Sep 17 00:00:00 2001 From: ivan_yakimkin Date: Thu, 21 May 2026 19:41:45 +0300 Subject: [PATCH 021/112] fix(antigravity): improve plan label fallback per review Prefer persisted tier when live subscription maps to an unknown label, and only return mapped tier IDs from extractCodeAssistTierId. Add regression test for fallback from providerSpecificData. Co-authored-by: Cursor --- open-sse/services/usage.ts | 22 ++++++++++++++++------ tests/unit/usage-service-hardening.test.ts | 7 +++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 0839e0c4d1..8222299cd4 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -1562,8 +1562,7 @@ function extractCodeAssistTierId(subscription: JsonRecord): string { const tierId = extractCodeAssistOnboardTierId(subscription); if (tierId === "legacy-tier") return ""; const upper = tierId.toUpperCase(); - if (mapCodeAssistTierIdToLabel(upper)) return upper; - return upper; + return mapCodeAssistTierIdToLabel(upper) ? upper : ""; } function mapCodeAssistTierIdToLabel(tierId: string): string | null { @@ -1636,15 +1635,26 @@ function mapCodeAssistSubscriptionToPlanLabel(subscriptionInfo: unknown): string return "Free"; } +const KNOWN_ANTIGRAVITY_PLAN_LABELS = new Set([ + "Ultra", + "Pro", + "Enterprise", + "Business", + "Plus", + "Lite", +]); + /** * Map raw loadCodeAssist tier data to short display labels (Antigravity Manager parity). */ function getAntigravityPlanLabel(subscriptionInfo: unknown, fallbackInfo?: unknown): string { - const plan = mapCodeAssistSubscriptionToPlanLabel(subscriptionInfo); - if (plan !== "Free") return plan; - + const livePlan = mapCodeAssistSubscriptionToPlanLabel(subscriptionInfo); const fallbackPlan = mapCodeAssistSubscriptionToPlanLabel(fallbackInfo); - return fallbackPlan !== "Free" ? fallbackPlan : plan; + + if (KNOWN_ANTIGRAVITY_PLAN_LABELS.has(livePlan)) return livePlan; + if (KNOWN_ANTIGRAVITY_PLAN_LABELS.has(fallbackPlan)) return fallbackPlan; + if (livePlan !== "Free") return livePlan; + return fallbackPlan !== "Free" ? fallbackPlan : livePlan; } /** diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index fafbde5af8..b31d2975a1 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -1299,6 +1299,13 @@ test("usage helper branches cover Gemini CLI and Antigravity plan label fallback }), "Custom sky" ); + assert.equal( + __testing.getAntigravityPlanLabel( + { currentTier: { name: "TIER_UNKNOWN_CUSTOM" } }, + { allowedTiers: [{ id: "tier_pro", isDefault: true }] } + ), + "Pro" + ); }); test("usage service covers NanoGPT PRO weekly token quota, FREE plan, auth denial and fetch failures", async () => { From 07df7c8e439e0ebd89539456ff829945d7c1c910 Mon Sep 17 00:00:00 2001 From: Automation Date: Thu, 21 May 2026 20:03:45 +0200 Subject: [PATCH 022/112] fix(opencode-zen): add 'opencode' provider alias and sync model list with live API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode's Zen provider changed its slug from 'opencode-zen' to 'opencode', breaking OmniRoute's provider resolution when users reference models with the new prefix (e.g. 'opencode/deepseek-v4-flash-free'). Changes: 1. open-sse/services/model.ts: Add manual ALIAS_TO_PROVIDER_ID entry mapping 'opencode' → 'opencode-zen' so parseModel() resolves correctly for model strings using the new slug. 2. open-sse/executors/index.ts: Register 'opencode' as an OpencodeExecutor alias for 'opencode-zen' so getExecutor() returns the correct executor. 3. open-sse/config/providerRegistry.ts: Update opencode-zen model list to match the live API at https://opencode.ai/zen/v1/models: - Add deepseek-v4-flash-free (the model users reported as broken) - Add all 30+ models from the API (Claude, GPT, Gemini, Grok, GLM, MiniMax, Kimi, Qwen series) - Apply targetFormat: 'claude' to qwen3.5-plus (same SSE bug as qwen3.6) - Remove ling-2.6-1t-free and trinity-large-preview-free (no longer in API) - Enable passthroughModels so new models work without code deploys 4. @omniroute/opencode-provider/src/index.ts: Remove broken reference to undefined OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS constant. 5. tests/unit/opencode-executor.test.ts: Add tests for opencode alias, deepseek-v4-flash-free routing, and model registry presence. --- @omniroute/opencode-provider/src/index.ts | 1 + open-sse/config/providerRegistry.ts | 72 +++++++++++++++++++---- open-sse/executors/index.ts | 1 + open-sse/services/model.ts | 5 ++ tests/unit/opencode-executor.test.ts | 20 +++++++ 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index 4e2f070a19..91078ef42a 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -235,6 +235,7 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open if (typeof merged.reasoning === "boolean") entry.reasoning = merged.reasoning; if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature; if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call; + models[id] = entry; } diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 6a2e82a660..8fcb24899e 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1244,23 +1244,71 @@ export const REGISTRY: Record = { authHeader: "Authorization", authPrefix: "Bearer", defaultContextLength: 200000, + // Sync with https://opencode.ai/zen/v1/models — this list is regenerated + // from the live API response so new models work without a code deploy. + passthroughModels: true, models: [ + // ── Chat / Coding ────────────────────────────────────────── { id: "big-pickle", name: "Big Pickle" }, { id: "gpt-5-nano", name: "GPT 5 Nano", contextLength: 400000 }, + { id: "gpt-5", name: "GPT 5" }, + { id: "gpt-5-codex", name: "GPT 5 Codex" }, + { id: "gpt-5.1", name: "GPT 5.1" }, + { id: "gpt-5.1-codex", name: "GPT 5.1 Codex" }, + { id: "gpt-5.1-codex-max", name: "GPT 5.1 Codex Max" }, + { id: "gpt-5.1-codex-mini", name: "GPT 5.1 Codex Mini" }, + { id: "gpt-5.2", name: "GPT 5.2" }, + { id: "gpt-5.2-codex", name: "GPT 5.2 Codex" }, + { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, + { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, + { id: "gpt-5.4", name: "GPT 5.4" }, + { id: "gpt-5.4-mini", name: "GPT 5.4 Mini" }, + { id: "gpt-5.4-nano", name: "GPT 5.4 Nano" }, + { id: "gpt-5.4-pro", name: "GPT 5.4 Pro" }, + { id: "gpt-5.5", name: "GPT 5.5" }, + { id: "gpt-5.5-pro", name: "GPT 5.5 Pro" }, + + // ── Claude ───────────────────────────────────────────────── + { id: "claude-haiku-4-5", name: "Claude Haiku 4.5" }, + { id: "claude-sonnet-4", name: "Claude Sonnet 4" }, + { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }, + { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + { id: "claude-opus-4-1", name: "Claude Opus 4.1" }, + { id: "claude-opus-4-5", name: "Claude Opus 4.5" }, + { id: "claude-opus-4-6", name: "Claude Opus 4.6" }, + { id: "claude-opus-4-7", name: "Claude Opus 4.7" }, + + // ── Gemini ───────────────────────────────────────────────── + { id: "gemini-3-flash", name: "Gemini 3 Flash" }, + { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, + { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" }, + + // ── Grok ─────────────────────────────────────────────────── + { id: "grok-build-0.1", name: "Grok Build 0.1" }, + + // ── GLM / Z.AI ───────────────────────────────────────────── + { id: "glm-5", name: "GLM-5" }, + { id: "glm-5.1", name: "GLM-5.1" }, + + // ── MiniMax ──────────────────────────────────────────────── + { id: "minimax-m2.5", name: "MiniMax M2.5" }, + { id: "minimax-m2.7", name: "MiniMax M2.7" }, + + // ── Kimi / Moonshot ──────────────────────────────────────── + { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "kimi-k2.6", name: "Kimi K2.6" }, + + // ── Qwen ─────────────────────────────────────────────────── + // Issue #2292: Qwen models return Claude-format SSE bodies even + // when hitting /chat/completions. targetFormat: "claude" routes + // through /messages and the Claude translator. + { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude" }, + { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude" }, + + // ── Free Tier ────────────────────────────────────────────── + { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true }, { id: "minimax-m2.5-free", name: "MiniMax M2.5 Free", contextLength: 204800 }, - { id: "ling-2.6-1t-free", name: "Ling 2.6 Free", contextLength: 262000 }, - { - id: "trinity-large-preview-free", - name: "Trinity Large Preview Free", - contextLength: 131000, - }, { id: "nemotron-3-super-free", name: "Nemotron 3 Super Free", contextLength: 1000000 }, - // Issue #2292: opencode-zen returns Claude-format SSE bodies for these - // Qwen3.6 models even when the request hits the OpenAI-compatible - // /chat/completions endpoint. Flagging targetFormat: "claude" routes - // the request to /messages and parses the response with the Claude - // translator, fixing "expected choices (array), received undefined". - { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", contextLength: 200000 }, { id: "qwen3.6-plus-free", name: "Qwen3.6 Plus Free", diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index c4afe15ef2..1ff6416dee 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -58,6 +58,7 @@ const executors = { cf: new CloudflareAIExecutor(), // Alias "opencode-zen": new OpencodeExecutor("opencode-zen"), "opencode-go": new OpencodeExecutor("opencode-go"), + opencode: new OpencodeExecutor("opencode-zen"), // Alias for opencode-zen puter: new PuterExecutor(), pu: new PuterExecutor(), // Alias vertex: new VertexExecutor(), diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 08c4aefdc1..be4dfa3c97 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -29,6 +29,11 @@ for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) { ALIAS_TO_PROVIDER_ID[alias] = id; } +// Manual aliases for external compatibility not covered by PROVIDER_ID_TO_ALIAS. +// OpenCode's Zen provider now uses the "opencode" slug, but OmniRoute registers +// it as "opencode-zen". This alias ensures `opencode/` resolves correctly. +ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen"; + // Provider-scoped legacy model aliases. Used to normalize provider/model inputs // and keep backward compatibility when upstream IDs change. const PROVIDER_MODEL_ALIASES: ProviderModelAliasMap = { diff --git a/tests/unit/opencode-executor.test.ts b/tests/unit/opencode-executor.test.ts index ca333b6551..72cf128582 100644 --- a/tests/unit/opencode-executor.test.ts +++ b/tests/unit/opencode-executor.test.ts @@ -56,6 +56,26 @@ describe("OpencodeExecutor", () => { }); describe("execute", () => { + it('resolves "opencode" executor alias to opencode-zen config', async () => { + const aliasExecutor = new OpencodeExecutor("opencode-zen"); + const result = await aliasExecutor.execute(createInput("deepseek-v4-flash-free")); + assert.equal(result.url, "https://opencode.ai/zen/v1/chat/completions"); + assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions"); + }); + + it("routes deepseek-v4-flash-free to chat completions", async () => { + const result = await zenExecutor.execute(createInput("deepseek-v4-flash-free")); + assert.equal(result.url, "https://opencode.ai/zen/v1/chat/completions"); + assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions"); + }); + + it("includes deepseek-v4-flash-free in opencode-zen PROVIDER_MODELS", () => { + const models = PROVIDER_MODELS["opencode-zen"]; + const model = models?.find((m) => m.id === "deepseek-v4-flash-free"); + assert.ok(model, "deepseek-v4-flash-free should be in opencode-zen model list"); + assert.equal(model.name, "DeepSeek V4 Flash Free"); + assert.equal(model.supportsReasoning, true); + }); it("routes opencode zen default models to chat completions", async () => { const minimaxResult = await zenExecutor.execute(createInput("minimax-m2.5-free")); assert.equal(minimaxResult.url, "https://opencode.ai/zen/v1/chat/completions"); From 7be33531b2d5a4e990006dc1ffbac4ebc74ba562 Mon Sep 17 00:00:00 2001 From: Apostol Apostolov Date: Thu, 21 May 2026 21:52:23 +0300 Subject: [PATCH 023/112] fix(dark-mode): correct background token on Compression Override select (#2513) Integrated into release/v3.8.2 --- src/app/(dashboard)/dashboard/combos/page.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 483c13de89..445366fb50 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -1737,15 +1737,15 @@ function ComboCard({ value={compressionOverride} onChange={(e) => handleCompressionOverrideChange(e.target.value)} disabled={isSavingCompression} - className="text-xs py-1 px-2 rounded border border-black/10 dark:border-white/10 bg-white dark:bg-bg-main text-text-main focus:border-primary focus:outline-none transition-colors disabled:opacity-50 max-w-[130px] md:max-w-none" + className="text-xs py-1 px-2 rounded border border-black/10 dark:border-white/10 bg-surface text-text-main focus:border-primary focus:outline-none transition-colors disabled:opacity-50 max-w-[130px] md:max-w-none" title={t("compressionOverride")} > - - - - - - + + + + + + )} + {authUrl.length > 80 ? authUrl.slice(0, 80) + "..." : authUrl} + + - -
-

Step 2: Paste the callback URL here

-

- After authorization, copy the full URL from your browser address bar. -

- setCallbackUrl(e.target.value)} - placeholder="kiro://kiro.kiroAgent/authenticate-success?code=..." - className="font-mono text-xs" - /> + )} + {userCode && ( +
+

Verification code

+

{userCode}

+ )} +
+ + progress_activity + + Waiting for authorization...
- -
- -
- +
)} - {/* Success */} {step === "success" && (
@@ -188,13 +173,12 @@ export default function KiroSocialOAuthModal({

Your {providerLabel} account via {providerName} has been connected.

-
)} - {/* Error */} {step === "error" && (
@@ -203,11 +187,8 @@ export default function KiroSocialOAuthModal({

Connection Failed

{error}

- -
From 32804f2a43daac05d149b40aee3d293b0fe51f6d Mon Sep 17 00:00:00 2001 From: Hernan Javier Ardila Sanchez Date: Thu, 21 May 2026 23:10:17 +0200 Subject: [PATCH 040/112] fix(opencode-zen): add 'opencode' provider alias and sync model list with live API (#2517) Integrated into release/v3.8.2 --- open-sse/services/model.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 1f803c180c..8ded9dff24 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -28,6 +28,12 @@ for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) { } ALIAS_TO_PROVIDER_ID[alias] = id; } +// Manual alias overrides — maps slug-style prefixes to canonical provider IDs. +// These live outside the registry because they represent multiple providers +// or backward-compatible slug changes, not a single provider's display name. +// opencode/ → opencode-zen (the main free/open tier; opencode-go is a separate paid tier) +ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen"; + // Manual aliases for external compatibility not covered by PROVIDER_ID_TO_ALIAS. // OpenCode's Zen provider now uses the "opencode" slug, but OmniRoute registers From 1f8b41bc23e198d0efaabe0bf47bbcd1453918b3 Mon Sep 17 00:00:00 2001 From: InkshadeWoods <144514307+InkshadeWoods@users.noreply.github.com> Date: Fri, 22 May 2026 05:10:32 +0800 Subject: [PATCH 041/112] fix(i18n): translate 830 missing zh-CN UI strings (#2523) Integrated into release/v3.8.2 --- src/i18n/messages/zh-CN.json | 1660 +++++++++++++++++----------------- 1 file changed, 830 insertions(+), 830 deletions(-) diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 17e19ec123..140064815b 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -158,7 +158,7 @@ "noLockouts": "无锁定", "webSearchDesc": "Web Search 功能说明", "audioProvidersHeading": "音频 Provider", - "cloudAgentProviders": "__MISSING__:Cloud Agent Providers", + "cloudAgentProviders": "云端 Agent 提供商", "minutesAgo": "分钟前", "a": "A", "liveAutoRefreshing": "实时自动刷新中", @@ -365,10 +365,10 @@ "apiKeyForCheck": "用于检查的 API 密钥", "cloudRequestTimeout": "云端请求超时", "showConfiguredOnly": "显示已配置仅", - "showFreeOnly": "__MISSING__:Free only", - "addFirstProvider": "__MISSING__:Add your first provider", - "addFirstProviderDesc": "__MISSING__:Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts.", - "learnMore": "__MISSING__:Learn more", + "showFreeOnly": "仅免费", + "addFirstProvider": "添加你的第一个提供商", + "addFirstProviderDesc": "连接一个 AI 提供商,开始通过 OmniRoute 路由请求。你可以使用免费提供商、API 密钥或 OAuth 账号。", + "learnMore": "了解更多", "includeDomains": "包含域名", "promptCache": "提示缓存", "cloudConnected": "Cloud 已连接", @@ -598,9 +598,9 @@ "syncingData": "正在同步数据", "cloudBenefitPorts": "通过 Cloud 暴露端口", "compatibleProviders": "兼容 Provider", - "freeTierProviders": "__MISSING__:Free Tier Providers", - "freeTierLabel": "__MISSING__:Free tier available", - "freeTierProvidersDesc": "__MISSING__:Providers with free tiers — some require an API key signup, others need no credentials at all.", + "freeTierProviders": "免费层提供商", + "freeTierLabel": "提供免费层", + "freeTierProvidersDesc": "提供免费层的提供商:有些需要注册 API 密钥,有些完全不需要凭据。", "clearCache": "清除缓存", "reqs": "请求数", "addAnthropicCompatible": "添加 Anthropic 兼容 Provider", @@ -657,55 +657,55 @@ "learnedFromHeaders": "从响应头学习", "totalRequests": "总计请求", "cloudUnstableNote": "Cloud 连接不稳定,部分功能可能受影响。", - "gamificationAdmin": "__MISSING__:Gamification Admin", - "monitorAnomaliesAndHealth": "__MISSING__:Monitor anomalies and system health", - "flaggedAnomalies": "__MISSING__:Flagged Anomalies", - "noAnomaliesDetected": "__MISSING__:No anomalies detected", - "apiKey": "__MISSING__:API Key", - "xpLastHour": "__MISSING__:XP (1h)", - "zScore": "__MISSING__:Z-Score", - "tokensCommunityServers": "__MISSING__:Community Servers", - "tokensServerNamePlaceholder": "__MISSING__:Server name", - "tokensApiKeyPlaceholder": "__MISSING__:API key", - "tokensTokenBalance": "__MISSING__:Token Balance", - "tokensSendTokens": "__MISSING__:Send Tokens", - "tokensRecipientApiKeyId": "__MISSING__:Recipient API Key ID", - "tokensRecipientApiKeyIdPlaceholder": "__MISSING__:Enter recipient API key ID", - "tokensReasonOptional": "__MISSING__:Reason (optional)", - "tokensReasonPlaceholder": "__MISSING__:e.g. bonus, reward", - "tokensTransactionHistory": "__MISSING__:Transaction History", - "tokensNoTransactionsYet": "__MISSING__:No transactions yet", - "tokensInviteCodes": "__MISSING__:Invite Codes", - "tokensMaxUses": "__MISSING__:Max Uses", - "tokensRedeemCode": "__MISSING__:Redeem Code", - "tokensRedeemCodePlaceholder": "__MISSING__:Enter invite code", - "tokensYourActiveInvites": "__MISSING__:Your Active Invites", - "tierCoverageTitle": "__MISSING__:Tier coverage", - "tierCoverageSubtitle": "__MISSING__:Providers configured per fallback tier", - "batchDetailCopyId": "__MISSING__:Copy ID", - "batchDetailClose": "__MISSING__:Close", - "batchDetailEndpoint": "__MISSING__:Endpoint", - "batchDetailModel": "__MISSING__:Model", - "batchDetailWindow": "__MISSING__:Window", - "batchDetailCreated": "__MISSING__:Created", - "providerTopologyEmpty": "__MISSING__:No providers connected yet", - "badgeToastUnlocked": "__MISSING__:Badge Unlocked!", - "batchListSearchPlaceholder": "__MISSING__:Search by ID, endpoint, model…", - "batchListDeleteAllCompletedTitle": "__MISSING__:Delete all completed batches", - "batchListBatchesTable": "__MISSING__:Batches", - "changelogViewerLoading": "__MISSING__:Loading changelog from GitHub...", - "profileLoading": "__MISSING__:Loading profile...", - "profileHowToEarn": "__MISSING__:How to earn", - "bootstrapBannerDismiss": "__MISSING__:Dismiss", - "batchListDeleteBatchTitle": "__MISSING__:Delete batch and its files", - "leaderboardYourRank": "__MISSING__:Your Rank", - "leaderboardLoading": "__MISSING__:Loading leaderboard...", - "batchFileDetailCopyId": "__MISSING__:Copy ID", - "batchFileDetailClose": "__MISSING__:Close", - "batchFileDetailFailedToLoad": "__MISSING__:Failed to load file contents", - "batchFilesListSearchPlaceholder": "__MISSING__:Search by ID or filename…", - "batchFilesListFilesTable": "__MISSING__:Files", - "batchPageLoadingMore": "__MISSING__:Loading more…" + "gamificationAdmin": "游戏化管理", + "monitorAnomaliesAndHealth": "监控异常和系统健康状态", + "flaggedAnomalies": "已标记异常", + "noAnomaliesDetected": "未检测到异常", + "apiKey": "API 密钥", + "xpLastHour": "XP(1 小时)", + "zScore": "Z 分数", + "tokensCommunityServers": "社区服务器", + "tokensServerNamePlaceholder": "服务器名称", + "tokensApiKeyPlaceholder": "API 密钥", + "tokensTokenBalance": "Token 余额", + "tokensSendTokens": "发送 Tokens", + "tokensRecipientApiKeyId": "接收方 API 密钥 ID", + "tokensRecipientApiKeyIdPlaceholder": "输入接收方 API 密钥 ID", + "tokensReasonOptional": "原因(可选)", + "tokensReasonPlaceholder": "例如:奖励、激励", + "tokensTransactionHistory": "交易历史", + "tokensNoTransactionsYet": "暂无交易", + "tokensInviteCodes": "邀请码", + "tokensMaxUses": "最大使用次数", + "tokensRedeemCode": "兑换代码", + "tokensRedeemCodePlaceholder": "输入邀请码", + "tokensYourActiveInvites": "你的有效邀请", + "tierCoverageTitle": "层级覆盖", + "tierCoverageSubtitle": "每个回退层级已配置的提供商", + "batchDetailCopyId": "复制 ID", + "batchDetailClose": "关闭", + "batchDetailEndpoint": "Endpoint", + "batchDetailModel": "模型", + "batchDetailWindow": "窗口", + "batchDetailCreated": "已创建", + "providerTopologyEmpty": "尚未连接提供商", + "badgeToastUnlocked": "已解锁徽章!", + "batchListSearchPlaceholder": "按 ID、Endpoint、模型搜索…", + "batchListDeleteAllCompletedTitle": "删除所有已完成批次", + "batchListBatchesTable": "批次", + "changelogViewerLoading": "正在从 GitHub 加载更新日志...", + "profileLoading": "正在加载个人资料...", + "profileHowToEarn": "如何获取", + "bootstrapBannerDismiss": "关闭", + "batchListDeleteBatchTitle": "删除批次及其文件", + "leaderboardYourRank": "你的排名", + "leaderboardLoading": "正在加载排行榜...", + "batchFileDetailCopyId": "复制 ID", + "batchFileDetailClose": "关闭", + "batchFileDetailFailedToLoad": "无法加载文件内容", + "batchFilesListSearchPlaceholder": "按 ID 或文件名搜索…", + "batchFilesListFilesTable": "文件", + "batchPageLoadingMore": "正在加载更多…" }, "sidebar": { "home": "首页", @@ -725,24 +725,24 @@ "playground": "演练场", "searchTools": "搜索工具", "agents": "智能体", - "cloudAgents": "__MISSING__:Cloud Agents", + "cloudAgents": "云端 Agents", "memory": "记忆", "skills": "技能", - "omniSkills": "__MISSING__:OmniSkills", - "agentSkills": "__MISSING__:AgentSkills", + "omniSkills": "OmniSkills", + "agentSkills": "AgentSkills", "docs": "文档", "issues": "问题反馈", "endpoints": "端点", - "endpointsSubtitle": "__MISSING__:Your AI connection URLs", + "endpointsSubtitle": "你的 AI 连接 URL", "apiManager": "API 管理", - "apiManagerSubtitle": "__MISSING__:Manage API keys and access", + "apiManagerSubtitle": "管理 API 密钥和访问权限", "logs": "日志", "webhooks": "Webhook", - "webhooksSubtitle": "__MISSING__:Get notified of events", - "combosSubtitle": "__MISSING__:Group providers for failover", - "batchSubtitle": "__MISSING__:Process multiple requests", - "contextCavemanSubtitle": "__MISSING__:Prompt compression", - "contextRtkSubtitle": "__MISSING__:Output filtering", + "webhooksSubtitle": "接收事件通知", + "combosSubtitle": "将提供商分组用于故障转移", + "batchSubtitle": "处理多个请求", + "contextCavemanSubtitle": "提示词压缩", + "contextRtkSubtitle": "输出过滤", "auditLog": "审计日志", "shutdown": "停止服务", "restart": "重启服务", @@ -836,12 +836,12 @@ "logsConsole": "Console", "logsActivity": "Activity", "auditMcp": "MCP Audit", - "auditA2a": "__MISSING__:A2A Audit", + "auditA2a": "A2A 审计", "settingsGeneral": "General", "settingsAppearance": "Appearance", "settingsAi": "AI Settings", "settingsSecurity": "Security", - "settingsFeatureFlags": "__MISSING__:Feature Flags", + "settingsFeatureFlags": "功能开关", "settingsRouting": "Routing", "settingsResilience": "Resilience", "settingsAdvanced": "Advanced", @@ -912,7 +912,7 @@ "settingsResilienceSubtitle": "Retries and breakers", "settingsAdvancedSubtitle": "Power user options", "settingsSecuritySubtitle": "Auth and encryption", - "settingsFeatureFlagsSubtitle": "__MISSING__:Toggle system capabilities", + "settingsFeatureFlagsSubtitle": "切换系统能力", "docsSubtitle": "Documentation", "issuesSubtitle": "Report a bug", "changelogSubtitle": "Release notes" @@ -1013,23 +1013,23 @@ "success": "成功", "failure": "失败", "noMcpEvents": "尚未记录 MCP 审计事件。", - "a2aAudit": "__MISSING__:A2A Audit", - "a2aAuditDesc": "__MISSING__:Task execution audit trail recorded by the A2A server.", - "a2aShowingTasks": "__MISSING__:Showing {count} of {total} tasks", - "a2aSkill": "__MISSING__:Skill", - "a2aSkillPlaceholder": "__MISSING__:Filter by skill name", - "a2aState": "__MISSING__:State", - "a2aAllStates": "__MISSING__:All states", - "a2aStateSubmitted": "__MISSING__:Submitted", - "a2aStateWorking": "__MISSING__:Working", - "a2aStateCompleted": "__MISSING__:Completed", - "a2aStateFailed": "__MISSING__:Failed", - "a2aStateCancelled": "__MISSING__:Cancelled", - "a2aTaskId": "__MISSING__:Task ID", - "a2aEvents": "__MISSING__:Events", - "a2aArtifacts": "__MISSING__:Artifacts", - "a2aNoTasks": "__MISSING__:No A2A tasks recorded.", - "a2aLoadingTasks": "__MISSING__:Loading A2A tasks..." + "a2aAudit": "A2A 审计", + "a2aAuditDesc": "由 A2A 服务器记录的任务执行审计轨迹。", + "a2aShowingTasks": "正在显示 {total} 个任务中的 {count} 个", + "a2aSkill": "技能", + "a2aSkillPlaceholder": "按技能名称筛选", + "a2aState": "状态", + "a2aAllStates": "所有状态", + "a2aStateSubmitted": "已提交", + "a2aStateWorking": "进行中", + "a2aStateCompleted": "已完成", + "a2aStateFailed": "失败", + "a2aStateCancelled": "已取消", + "a2aTaskId": "任务 ID", + "a2aEvents": "事件", + "a2aArtifacts": "产物", + "a2aNoTasks": "暂无 A2A 任务记录。", + "a2aLoadingTasks": "正在加载 A2A 任务..." }, "themesPage": { "title": "主题", @@ -1105,35 +1105,35 @@ "logsConsoleDescription": "Application console output and debug logs", "logsActivityDescription": "Audit trail of user actions and system events", "auditMcpDescription": "MCP tool invocation audit trail and compliance records", - "auditA2a": "__MISSING__:A2A Audit", - "auditA2aDescription": "__MISSING__:A2A task execution audit trail, state transitions, and skill invocation records", + "auditA2a": "A2A 审计", + "auditA2aDescription": "A2A 任务执行审计轨迹、状态转换和技能调用记录", "settingsGeneralDescription": "Storage, database, and general instance configuration", "settingsAppearanceDescription": "Theme, branding, and visual customization", "settingsAiDescription": "AI behaviors, thinking budgets, vision, and memory settings", "settingsSecurityDescription": "Authentication, authorization, and access control settings", - "featureFlags": "__MISSING__:Feature Flags", - "featureFlagsDescription": "__MISSING__:Control system capabilities and experimental features", - "featureFlagsActive": "__MISSING__:{count} active", - "featureFlagsInactive": "__MISSING__:{count} inactive", - "featureFlagsOverrides": "__MISSING__:{count} DB overrides", - "featureFlagsSearch": "__MISSING__:Search flags...", - "featureFlagsCategoryAll": "__MISSING__:All", - "featureFlagsCategorySecurity": "__MISSING__:Security", - "featureFlagsCategoryNetwork": "__MISSING__:Network", - "featureFlagsCategoryPolicies": "__MISSING__:Policies", - "featureFlagsCategoryRuntime": "__MISSING__:Runtime", - "featureFlagsCategoryCli": "__MISSING__:CLI", - "featureFlagsCategoryHealth": "__MISSING__:Health", - "featureFlagsSourceDb": "__MISSING__:DB", - "featureFlagsSourceEnv": "__MISSING__:ENV", - "featureFlagsSourceDefault": "__MISSING__:DEF", - "featureFlagsReset": "__MISSING__:Reset", - "featureFlagsResetAll": "__MISSING__:Reset All Overrides", - "featureFlagsResetAllConfirm": "__MISSING__:Are you sure you want to reset all feature flag overrides? This will revert all flags to their ENV or default values.", - "featureFlagsRestartRequired": "__MISSING__:Restart required to apply", - "featureFlagsNoResults": "__MISSING__:No flags match your search", - "featureFlagsSaved": "__MISSING__:Flag updated", - "featureFlagsError": "__MISSING__:Failed to update flag", + "featureFlags": "功能开关", + "featureFlagsDescription": "控制系统能力和实验性功能", + "featureFlagsActive": "{count} 个已启用", + "featureFlagsInactive": "{count} 个未启用", + "featureFlagsOverrides": "{count} 个数据库覆盖项", + "featureFlagsSearch": "搜索功能开关...", + "featureFlagsCategoryAll": "全部", + "featureFlagsCategorySecurity": "安全", + "featureFlagsCategoryNetwork": "网络", + "featureFlagsCategoryPolicies": "策略", + "featureFlagsCategoryRuntime": "运行时", + "featureFlagsCategoryCli": "CLI", + "featureFlagsCategoryHealth": "健康", + "featureFlagsSourceDb": "DB", + "featureFlagsSourceEnv": "ENV", + "featureFlagsSourceDefault": "DEF", + "featureFlagsReset": "重置", + "featureFlagsResetAll": "重置所有覆盖项", + "featureFlagsResetAllConfirm": "确定要重置所有功能开关覆盖项吗?这会将所有开关恢复为 ENV 或默认值。", + "featureFlagsRestartRequired": "需要重启后生效", + "featureFlagsNoResults": "没有匹配搜索条件的功能开关", + "featureFlagsSaved": "功能开关已更新", + "featureFlagsError": "更新功能开关失败", "settingsRoutingDescription": "Routing rules, model aliases, combo defaults, and degradation settings", "settingsResilienceDescription": "Circuit breaker, retry, and fallback configuration", "settingsAdvancedDescription": "Advanced payload rules, request limits, and proxy API settings", @@ -1155,8 +1155,8 @@ "providersOverview": "提供商概览", "configuredOf": "{total} 个可用提供商中已配置 {configured} 个", "noModelsAvailable": "该提供商当前没有可用模型。", - "noProvidersConfigured": "__MISSING__:No providers configured yet", - "addProvider": "__MISSING__:Add a provider", + "noProvidersConfigured": "尚未配置提供商", + "addProvider": "添加提供商", "configureFirst": "首先在 {providers} 中配置连接", "configureProvider": "配置提供商", "modelAvailable": "{count} 模型可用", @@ -1178,7 +1178,7 @@ "updating": "更新中...", "updateAvailableDesc": "有新版本可用。点击更新。", "updateStarted": "更新已开始...", - "reloadingPageAutomatically": "__MISSING__:Reloading page automatically...", + "reloadingPageAutomatically": "正在自动重新加载页面...", "providerTopology": "提供商拓扑" }, "analytics": { @@ -1197,50 +1197,50 @@ "comboHealthDescription": "组合级别配额、使用分布和性能指标", "compressionAnalyticsTitle": "压缩分析", "compressionAnalyticsDescription": "压缩分析 — token 节省、模式分布和提供商统计。", - "autoRoutingTotalAutoRequests": "__MISSING__:Total Auto Requests", - "autoRoutingAvgSelectionScore": "__MISSING__:Avg Selection Score", - "autoRoutingExplorationRate": "__MISSING__:Exploration Rate", - "autoRoutingLkgpHitRate": "__MISSING__:LKGP Hit Rate", - "autoRoutingRequestsByVariant": "__MISSING__:Requests by Variant", - "autoRoutingTopRoutedProviders": "__MISSING__:Top Routed Providers", - "comboHealthWorstQuotaLeft": "__MISSING__:Worst quota left", - "comboHealthUsageSkew": "__MISSING__:Usage skew", - "comboHealthSuccessRate": "__MISSING__:Success rate", - "comboHealthQuotaHealth": "__MISSING__:Quota health", - "comboHealthRequests": "__MISSING__:Requests", - "comboHealthTokens": "__MISSING__:Tokens", - "comboHealthAvgLatency": "__MISSING__:Avg latency", - "comboHealthTotalRequests": "__MISSING__:Total requests", - "comboHealthExecutionTargets": "__MISSING__:Execution targets", - "comboHealthSuccess": "__MISSING__:Success", - "comboHealthLatency": "__MISSING__:Latency", - "comboHealthQuota": "__MISSING__:Quota", - "comboHealthTitle": "__MISSING__:Combo health", - "comboHealthUnableToLoad": "__MISSING__:Unable to load combo health", - "comboHealthGettingStarted": "__MISSING__:Getting started", - "compressionAnalyticsTotalRequests": "__MISSING__:Total Requests", - "compressionAnalyticsTokensSaved": "__MISSING__:Tokens Saved", - "compressionAnalyticsAvgSavings": "__MISSING__:Avg Savings", - "compressionAnalyticsAvgDuration": "__MISSING__:Avg Duration", - "compressionAnalyticsReceipts": "__MISSING__:Receipts", - "compressionAnalyticsFallbacks": "__MISSING__:Fallbacks", - "compressionAnalyticsPromptTokens": "__MISSING__:Prompt tokens", - "compressionAnalyticsCompletionTokens": "__MISSING__:Completion tokens", - "compressionAnalyticsTotalTokens": "__MISSING__:Total tokens", - "compressionAnalyticsCacheTokens": "__MISSING__:Cache tokens", - "compressionAnalyticsNoDataYet": "__MISSING__:No compression data yet", - "searchAnalyticsTotalSearches": "__MISSING__:Total Searches", - "searchAnalyticsCacheHitRate": "__MISSING__:Cache Hit Rate", - "searchAnalyticsTotalCost": "__MISSING__:Total Cost", - "searchAnalyticsAvgResponse": "__MISSING__:Avg Response", - "searchAnalyticsNoSearchesYet": "__MISSING__:No searches yet", - "providerUtilizationTitle": "__MISSING__:Provider utilization", - "providerUtilizationFailedToLoad": "__MISSING__:Failed to load utilization data", - "providerUtilizationNoData": "__MISSING__:No utilization data available", - "providerUtilizationGettingStarted": "__MISSING__:Getting started", - "providerUtilizationLatestSnapshot": "__MISSING__:Latest quota snapshot", - "providerUtilizationRemainingCapacity": "__MISSING__:Remaining capacity", - "diversityScoreTitle": "__MISSING__:Provider Diversity" + "autoRoutingTotalAutoRequests": "Auto 请求总数", + "autoRoutingAvgSelectionScore": "平均选择分数", + "autoRoutingExplorationRate": "探索率", + "autoRoutingLkgpHitRate": "LKGP 命中率", + "autoRoutingRequestsByVariant": "按变体统计请求", + "autoRoutingTopRoutedProviders": "最常路由提供商", + "comboHealthWorstQuotaLeft": "最低剩余额度", + "comboHealthUsageSkew": "使用偏斜", + "comboHealthSuccessRate": "成功率", + "comboHealthQuotaHealth": "额度健康状态", + "comboHealthRequests": "请求", + "comboHealthTokens": "Tokens", + "comboHealthAvgLatency": "平均延迟", + "comboHealthTotalRequests": "请求总数", + "comboHealthExecutionTargets": "执行目标", + "comboHealthSuccess": "成功", + "comboHealthLatency": "延迟", + "comboHealthQuota": "额度", + "comboHealthTitle": "组合健康状态", + "comboHealthUnableToLoad": "无法加载组合健康状态", + "comboHealthGettingStarted": "快速开始", + "compressionAnalyticsTotalRequests": "请求总数", + "compressionAnalyticsTokensSaved": "已节省 Tokens", + "compressionAnalyticsAvgSavings": "平均节省量", + "compressionAnalyticsAvgDuration": "平均耗时", + "compressionAnalyticsReceipts": "回执", + "compressionAnalyticsFallbacks": "回退", + "compressionAnalyticsPromptTokens": "Prompt tokens", + "compressionAnalyticsCompletionTokens": "Completion tokens", + "compressionAnalyticsTotalTokens": "总 tokens", + "compressionAnalyticsCacheTokens": "缓存 tokens", + "compressionAnalyticsNoDataYet": "暂无压缩数据", + "searchAnalyticsTotalSearches": "搜索总数", + "searchAnalyticsCacheHitRate": "缓存命中率", + "searchAnalyticsTotalCost": "总成本", + "searchAnalyticsAvgResponse": "平均响应", + "searchAnalyticsNoSearchesYet": "暂无搜索", + "providerUtilizationTitle": "提供商使用率", + "providerUtilizationFailedToLoad": "无法加载使用率数据", + "providerUtilizationNoData": "暂无可用的使用率数据", + "providerUtilizationGettingStarted": "快速开始", + "providerUtilizationLatestSnapshot": "最新额度快照", + "providerUtilizationRemainingCapacity": "剩余容量", + "diversityScoreTitle": "提供商多样性" }, "apiManager": { "title": "API 密钥", @@ -1342,30 +1342,30 @@ "lastUsedOn": "最近使用:{date}", "editPermissions": "编辑权限", "deleteKey": "删除密钥", - "regenerateKey": "__MISSING__:Regenerate key", - "regenerateConfirm": "__MISSING__:Are you sure you want to regenerate this API key? The old key will be immediately invalidated.", - "failedRegenerateKey": "__MISSING__:Failed to regenerate API key", - "failedRegenerateKeyRetry": "__MISSING__:Failed to regenerate API key. Please try again.", + "regenerateKey": "重新生成密钥", + "regenerateConfirm": "确定要重新生成此 API 密钥吗?旧密钥将立即失效。", + "failedRegenerateKey": "重新生成 API 密钥失败", + "failedRegenerateKeyRetry": "重新生成 API 密钥失败。请重试。", "model": "{count} 个模型", "models": "{count} 个模型", "permissionsTitle": "权限:{name}", "allowAllDesc": "该密钥可访问所有可用模型。", "restrictDesc": "该密钥可访问 {totalModels} 个模型中的 {selectedCount} 个。", "selectedCount": "已选择 {count} 个", - "maxActiveSessions": "__MISSING__:Max Active Sessions", - "apiManagerCustomRateLimits": "__MISSING__:Custom Rate Limits", - "apiManagerCustomRateLimitsDesc": "__MISSING__:Override global default limits. Leave empty to use defaults.", - "apiManagerRateLimitRequestsPlaceholder": "__MISSING__:Requests", - "apiManagerRateLimitReqPer": "__MISSING__:req /", - "apiManagerRateLimitSecondsPlaceholder": "__MISSING__:Seconds", - "apiManagerRemoveLimitTitle": "__MISSING__:Remove limit", - "apiManagerTimezonePlaceholder": "__MISSING__:America/Sao_Paulo", - "noLogPayloadPrivacy": "__MISSING__:No-Log Payload Privacy", - "bannedStatus": "__MISSING__:Banned Status", - "managementApiAccess": "__MISSING__:Management API Access", - "expirationDate": "__MISSING__:Expiration Date", - "managementAccess": "__MISSING__:Management Access", - "allowedConnections": "__MISSING__:Allowed Connections" + "maxActiveSessions": "最大活跃会话数", + "apiManagerCustomRateLimits": "自定义速率限制", + "apiManagerCustomRateLimitsDesc": "覆盖全局默认限制。留空则使用默认值。", + "apiManagerRateLimitRequestsPlaceholder": "请求", + "apiManagerRateLimitReqPer": "请求 /", + "apiManagerRateLimitSecondsPlaceholder": "秒", + "apiManagerRemoveLimitTitle": "移除限制", + "apiManagerTimezonePlaceholder": "Asia/Shanghai", + "noLogPayloadPrivacy": "无日志 Payload 隐私", + "bannedStatus": "封禁状态", + "managementApiAccess": "管理 API 访问", + "expirationDate": "过期日期", + "managementAccess": "管理访问", + "allowedConnections": "允许的连接" }, "auditLog": { "title": "审核日志", @@ -1454,8 +1454,8 @@ "rerankModel": "重排模型", "positionDelta": "排名变化", "emptyState": "发送搜索请求后即可查看结果", - "copy": "__MISSING__:Copy", - "resetToDefault": "__MISSING__:Reset to default" + "copy": "复制", + "resetToDefault": "重置为默认值" }, "cliTools": { "title": "CLI 工具", @@ -1777,33 +1777,33 @@ "customCliTab": "自定义 CLI", "toolCategories": "工具分类", "visibleToolsCount": "{count} 个工具可用", - "customCliBuilderTitle": "__MISSING__:OpenAI-compatible CLI builder", - "customCliBuilderDescription": "__MISSING__:Generate env vars and JSON snippets for any CLI or SDK that accepts an OpenAI-compatible base URL, API key, and model ID.", - "customCliNoModels": "__MISSING__:Connect at least one provider to populate the model selectors.", - "customCliNameLabel": "__MISSING__:CLI name", - "customCliNamePlaceholder": "__MISSING__:e.g. My Team CLI", - "customCliDefaultModelLabel": "__MISSING__:Default model", - "customCliDefaultModelHelp": "__MISSING__:Use any OmniRoute model ID or combo. Most OpenAI-compatible CLIs only need the /v1 base URL plus a model string.", - "customCliKeyHelper": "__MISSING__:For local installs OmniRoute can use sk_omniroute. In cloud mode, pick one of your management API keys.", - "customCliAliasMappingsLabel": "__MISSING__:Alias mappings", - "customCliAliasMappingsHelp": "__MISSING__:Optional helper aliases for wrapper scripts or config files that want stable shorthand names.", - "customCliAddAlias": "__MISSING__:Add alias", - "customCliNoMappings": "__MISSING__:No alias mappings yet. Add one if your wrapper or team scripts use stable short names.", - "customCliAliasPlaceholder": "__MISSING__:e.g. review", - "customCliTargetModelLabel": "__MISSING__:Target model", - "customCliEndpointHintLabel": "__MISSING__:How to wire the endpoint", - "customCliEndpointHint": "__MISSING__:Point any OpenAI-compatible client to the OmniRoute /v1 base URL. The raw chat completions endpoint is {endpoint}. Use the JSON block when the tool wants a provider object, or the env script when it reads OPENAI_* variables.", - "customCliEnvBlockTitle": "__MISSING__:Env / shell snippet", - "customCliJsonBlockTitle": "__MISSING__:Provider JSON block", - "copilotConfigGenerator": "__MISSING__:GitHub Copilot Config Generator", - "copilotApiKey": "__MISSING__:API Key", - "copilotFilterModelsPlaceholder": "__MISSING__:Filter models...", - "copilotMaxInputTokens": "__MISSING__:Max Input Tokens", - "copilotMaxOutputTokens": "__MISSING__:Max Output Tokens", - "copilotToolCalling": "__MISSING__:Tool Calling", - "copilotPasteInto": "__MISSING__:Paste into: ", - "wireApiChatCompletions": "__MISSING__:Chat Completions (/chat/completions)", - "wireApiResponses": "__MISSING__:Responses API (/responses)" + "customCliBuilderTitle": "OpenAI 兼容 CLI 构建器", + "customCliBuilderDescription": "为任何支持 OpenAI 兼容 base URL、API 密钥和模型 ID 的 CLI 或 SDK 生成环境变量和 JSON 片段。", + "customCliNoModels": "至少连接一个提供商以填充模型选择器。", + "customCliNameLabel": "CLI 名称", + "customCliNamePlaceholder": "例如:我的团队 CLI", + "customCliDefaultModelLabel": "默认模型", + "customCliDefaultModelHelp": "使用任意 OmniRoute 模型 ID 或组合。大多数 OpenAI 兼容 CLI 只需要 /v1 base URL 和一个模型字符串。", + "customCliKeyHelper": "本地安装时 OmniRoute 可以使用 sk_omniroute。云模式下请选择一个管理 API 密钥。", + "customCliAliasMappingsLabel": "别名映射", + "customCliAliasMappingsHelp": "为需要稳定简写名称的包装脚本或配置文件提供可选辅助别名。", + "customCliAddAlias": "添加别名", + "customCliNoMappings": "暂无别名映射。如果你的包装脚本或团队脚本使用稳定短名称,可以添加一个。", + "customCliAliasPlaceholder": "例如:review", + "customCliTargetModelLabel": "目标模型", + "customCliEndpointHintLabel": "如何接入 Endpoint", + "customCliEndpointHint": "将任何 OpenAI 兼容客户端指向 OmniRoute 的 /v1 base URL。原始 chat completions Endpoint 为 {endpoint}。当工具需要 provider 对象时使用 JSON 块;当工具读取 OPENAI_* 变量时使用 env 脚本。", + "customCliEnvBlockTitle": "Env / shell 片段", + "customCliJsonBlockTitle": "提供商 JSON 块", + "copilotConfigGenerator": "GitHub Copilot 配置生成器", + "copilotApiKey": "API 密钥", + "copilotFilterModelsPlaceholder": "筛选模型...", + "copilotMaxInputTokens": "最大输入 Tokens", + "copilotMaxOutputTokens": "最大输出 Tokens", + "copilotToolCalling": "工具调用", + "copilotPasteInto": "粘贴到:", + "wireApiChatCompletions": "Chat Completions(/chat/completions)", + "wireApiResponses": "Responses API(/responses)" }, "combos": { "title": "组合", @@ -1866,8 +1866,8 @@ "randomDesc": "均匀随机选择,失败后回退到剩余模型", "leastUsedDesc": "优先选择请求数最少的模型,随时间平衡负载", "costOptimizedDesc": "根据定价优先路由到最便宜的模型", - "resetAware": "__MISSING__:Reset-Aware RR", - "resetAwareDesc": "__MISSING__:Balances remaining quota against 5h and weekly resets, then round-robins similar scores", + "resetAware": "重置感知轮询", + "resetAwareDesc": "根据剩余额度、5 小时重置和每周重置进行平衡,然后对相近分数进行轮询", "strictRandom": "严格随机", "strictRandomDesc": "洗牌池模式:每个模型使用一次后再重新洗牌", "models": "模型", @@ -1884,9 +1884,9 @@ "contextRelaySummaryModelHelp": "仅用于生成交接摘要的可选覆盖模型。留空则复用当前活跃的 combo 模型。", "contextRelayProviderNote": "Context Relay 当前主要为 Codex 账户轮换生成交接摘要。与同一提供商的多个账户配合使用时,连续性效果最佳。", "advancedHint": "留空则使用全局默认值。这些设置会覆盖每个提供商的配置。", - "failoverBeforeRetry": "__MISSING__:Failover Before Retry", - "maxSetRetries": "__MISSING__:Max Set Retries", - "setRetryDelayMs": "__MISSING__:Set Retry Delay (ms)", + "failoverBeforeRetry": "重试前先故障转移", + "maxSetRetries": "最大集合重试次数", + "setRetryDelayMs": "集合重试延迟(毫秒)", "moveUp": "上移", "moveDown": "下移", "removeModel": "移除", @@ -1930,9 +1930,9 @@ "example": "后台任务或批处理作业,优先考虑更低成本。" }, "reset-aware": { - "when": "__MISSING__:You route across multiple accounts with quota telemetry and different reset windows.", - "avoid": "__MISSING__:Quota telemetry is unavailable for most accounts.", - "example": "__MISSING__:Prefer a 60% weekly account resetting tomorrow over an 80% account resetting later." + "when": "你会在多个账号之间路由,并且这些账号有额度遥测和不同的重置窗口。", + "avoid": "大多数账号没有可用的额度遥测。", + "example": "相比稍后重置的 80% 账号,优先选择明天重置的 60% 每周额度账号。" }, "strict-random": { "when": "适用于希望实现绝对均匀分配的场景,每个模型在重复前都会恰好使用一次。", @@ -1977,9 +1977,9 @@ "healthcheck": "路由决策时跳过不健康的模型或提供商。", "concurrencyPerModel": "轮询模式下每个模型允许的最大并发请求数。", "queueTimeout": "请求在队列中等待超时前允许停留的最长时间。", - "failoverBeforeRetry": "__MISSING__:When enabled, any upstream error triggers immediate failover to the next combo target, skipping all retries and fallback URLs.", - "maxSetRetries": "__MISSING__:Number of times to retry the full target set when every target fails. 0 = no set-level retry.", - "setRetryDelayMs": "__MISSING__:Delay between set-level retry attempts, giving transient issues time to resolve." + "failoverBeforeRetry": "启用后,任何上游错误都会立即故障转移到下一个组合目标,并跳过所有重试和备用 URL。", + "maxSetRetries": "当所有目标都失败时重试完整目标集合的次数。0 表示不进行集合级重试。", + "setRetryDelayMs": "集合级重试之间的延迟,用于给临时问题留出恢复时间。" }, "templatesTitle": "快捷模板", "templatesDescription": "先套用一个初始模板,再按需调整模型和配置。", @@ -2110,11 +2110,11 @@ "tip3": "适合批处理或后台任务等成本是主要指标的场景。" }, "reset-aware": { - "title": "__MISSING__:Reset-aware account rotation", - "description": "__MISSING__:Balances remaining provider quota against reset timing.", - "tip1": "__MISSING__:Use explicit account steps or account-tag routing for providers with quota telemetry.", - "tip2": "__MISSING__:Tune session vs weekly weights when short-term exhaustion is more risky.", - "tip3": "__MISSING__:Keep the tie band small so equivalent accounts still rotate fairly." + "title": "重置感知账号轮换", + "description": "根据提供商剩余额度和重置时间进行平衡。", + "tip1": "对于有额度遥测的提供商,使用显式账号步骤或账号标签路由。", + "tip2": "当短期耗尽风险更高时,调整会话权重和每周权重。", + "tip3": "保持较小的平局区间,让等价账号仍能公平轮换。" }, "strict-random": { "title": "洗牌池分配", @@ -2269,13 +2269,13 @@ "agentFeaturesToolFilterHint": "只有名称匹配此正则的工具才会转发给供应商。留空则转发所有工具。", "agentFeaturesContextCacheHint": "跨轮次锁定供应商/模型以保持缓存会话。内部标签在转发给供应商前会被移除。", "agentFeaturesContextCacheProtection": "上下文缓存保护", - "agentFeaturesContextLength": "__MISSING__:Context length", - "agentFeaturesContextLengthPlaceholder": "__MISSING__:e.g. 128000", - "agentFeaturesContextLengthHint": "__MISSING__:Defines the context window for this combo in /v1/models.", - "agentFeaturesContextLengthErrorInteger": "__MISSING__:Context length must be a valid integer", - "agentFeaturesContextLengthErrorRange": "__MISSING__:Context length must be between 1000 and 2000000", - "compressionOverride": "__MISSING__:Compression Override", - "modePack": "__MISSING__:Mode Pack" + "agentFeaturesContextLength": "上下文长度", + "agentFeaturesContextLengthPlaceholder": "例如:128000", + "agentFeaturesContextLengthHint": "定义此组合在 /v1/models 中展示的上下文窗口。", + "agentFeaturesContextLengthErrorInteger": "上下文长度必须是有效整数", + "agentFeaturesContextLengthErrorRange": "上下文长度必须在 1000 到 2000000 之间", + "compressionOverride": "压缩覆盖", + "modePack": "模式包" }, "costs": { "title": "成本", @@ -2472,14 +2472,14 @@ "a2aQuickStartStep3": "使用 `tasks/get` 与 `tasks/cancel` 跟踪和控制任务。", "completionsLegacy": "Completions(旧版)", "completionsLegacyDesc": "旧版 OpenAI 文本补全接口,同时接受 `prompt` 字符串和 `messages` 数组格式", - "messagesApi": "__MISSING__:Messages", - "messagesApiDesc": "__MISSING__:Native Anthropic Messages API format for Claude-compatible providers", - "imageEdits": "__MISSING__:Image Edits", - "imageEditsDesc": "__MISSING__:Edit and modify existing images with AI (inpainting, outpainting, variations)", - "batchApi": "__MISSING__:Batch API", - "batchApiDesc": "__MISSING__:Process large batches of requests asynchronously (OpenAI-compatible)", - "filesApi": "__MISSING__:Files API", - "filesApiDesc": "__MISSING__:Upload and manage files for batch processing", + "messagesApi": "消息", + "messagesApiDesc": "面向 Claude 兼容提供商的原生 Anthropic Messages API 格式", + "imageEdits": "图像编辑", + "imageEditsDesc": "使用 AI 编辑和修改现有图像(局部重绘、扩图、变体)", + "batchApi": "Batch API", + "batchApiDesc": "异步处理大批量请求(OpenAI 兼容)", + "filesApi": "Files API", + "filesApiDesc": "上传并管理用于批处理的文件", "videoGeneration": "视频生成", "videoDesc": "使用 ComfyUI 和 Stable Video Diffusion 等 AI 模型生成视频。", "tailscaleRequestFailed": "加载 Tailscale 状态失败", @@ -2532,17 +2532,17 @@ "ngrokStarted": "ngrok 隧道已启动", "ngrokStopped": "ngrok 隧道已停止", "ngrokRequestFailed": "更新 ngrok 隧道失败", - "tokenSaverSubtitle": "__MISSING__:Spend less tokens on every request.", - "tokenSaverToolOutput": "__MISSING__:Tool output", - "tokenSaverLlmOutput": "__MISSING__:LLM output", - "tokenSaverInputCompression": "__MISSING__:Input compression", - "apiEndpointsCatalogUnavailable": "__MISSING__:API catalog unavailable", - "apiEndpointsSearchPlaceholder": "__MISSING__:Search endpoints...", - "apiEndpointsRequiresAuth": "__MISSING__:Requires auth", - "apiEndpointsNoMatch": "__MISSING__:No endpoints match your filter", - "localServer": "__MISSING__:Local Server", - "cloudOmniroute": "__MISSING__:Cloud OmniRoute", - "copyUrl": "__MISSING__:Copy URL" + "tokenSaverSubtitle": "让每次请求消耗更少 Tokens。", + "tokenSaverToolOutput": "工具输出", + "tokenSaverLlmOutput": "LLM 输出", + "tokenSaverInputCompression": "输入压缩", + "apiEndpointsCatalogUnavailable": "API 目录不可用", + "apiEndpointsSearchPlaceholder": "搜索 Endpoints...", + "apiEndpointsRequiresAuth": "需要认证", + "apiEndpointsNoMatch": "没有 Endpoint 匹配你的筛选条件", + "localServer": "本地服务器", + "cloudOmniroute": "云端 OmniRoute", + "copyUrl": "复制 URL" }, "endpoints": { "tabProxy": "端点代理", @@ -2627,7 +2627,7 @@ "offset": "偏移量", "limit": "限制", "tool": "工具", - "mcpDashboardCopyUrl": "__MISSING__:Copy URL" + "mcpDashboardCopyUrl": "复制 URL" }, "a2aDashboard": { "loading": "正在加载 A2A 仪表板...", @@ -2686,12 +2686,12 @@ "offset": "偏移量", "limit": "限制", "skill": "Skill", - "rpcEndpoint": "__MISSING__:POST /a2a", - "rpcMethodSend": "__MISSING__:message/send", - "rpcMethodStream": "__MISSING__:message/stream", - "rpcMethodGet": "__MISSING__:tasks/get", - "rpcMethodCancel": "__MISSING__:tasks/cancel", - "serviceLabel": "__MISSING__:A2A" + "rpcEndpoint": "POST /a2a", + "rpcMethodSend": "message/send", + "rpcMethodStream": "message/stream", + "rpcMethodGet": "tasks/get", + "rpcMethodCancel": "tasks/cancel", + "serviceLabel": "A2A" }, "memory": { "title": "记忆管理", @@ -2718,18 +2718,18 @@ "procedural": "程序型", "semantic": "语义型", "a": "A", - "pipelineOk": "__MISSING__:Pipeline OK ({latencyMs}ms)", - "pipelineError": "__MISSING__:Pipeline error", - "healthUnknown": "__MISSING__:Health unknown", - "checkingHealth": "__MISSING__:Checking…", - "checkHealth": "__MISSING__:Check health", - "pageInfo": "__MISSING__:Page {page} of {totalPages} ({total} total)", - "previous": "__MISSING__:Previous", - "next": "__MISSING__:Next", - "cancel": "__MISSING__:Cancel", - "save": "__MISSING__:Save", - "keyPlaceholder": "__MISSING__:e.g. user.preferences.theme", - "contentPlaceholder": "__MISSING__:Value or JSON content to remember" + "pipelineOk": "管道正常({latencyMs}ms)", + "pipelineError": "管道错误", + "healthUnknown": "健康状态未知", + "checkingHealth": "正在检查…", + "checkHealth": "检查健康状态", + "pageInfo": "第 {page} 页,共 {totalPages} 页(总计 {total} 条)", + "previous": "上一页", + "next": "下一页", + "cancel": "取消", + "save": "保存", + "keyPlaceholder": "例如:user.preferences.theme", + "contentPlaceholder": "要记住的值或 JSON 内容" }, "skills": { "title": "技能", @@ -2759,10 +2759,10 @@ "networkAccessDesc": "允许发起出站网络请求", "mode": "模式", "q": "Q", - "filterSkillsPlaceholder": "__MISSING__:Filter skills by name, description, or tag", - "allModes": "__MISSING__:All modes", - "skillsMarketplace": "__MISSING__:Skills Marketplace", - "installSkill": "__MISSING__:Install Skill" + "filterSkillsPlaceholder": "按名称、描述或标签筛选技能", + "allModes": "所有模式", + "skillsMarketplace": "技能市场", + "installSkill": "安装技能" }, "health": { "title": "系统健康状况", @@ -2842,12 +2842,12 @@ "remainingOfLimit": "剩余 {remaining}/{limit}", "throttleStatus": "限流:{value}", "lastHeaderUpdate": "响应头更新:{age}", - "databaseHealth": "__MISSING__:Database Health", - "stickyBoundSessions": "__MISSING__:Sticky-bound sessions", - "sessionsByApiKey": "__MISSING__:Sessions by API key", - "noActiveSessionsTracked": "__MISSING__:No active sessions tracked yet.", - "noSessionQuotaMonitorsActive": "__MISSING__:No session quota monitors active.", - "gracefulDegradationStatus": "__MISSING__:Graceful Degradation Status" + "databaseHealth": "数据库健康状态", + "stickyBoundSessions": "粘性绑定会话", + "sessionsByApiKey": "按 API 密钥统计会话", + "noActiveSessionsTracked": "尚未跟踪到活跃会话。", + "noSessionQuotaMonitorsActive": "暂无活跃的会话额度监控。", + "gracefulDegradationStatus": "优雅降级状态" }, "telemetry": { "title": "系统遥测", @@ -3036,24 +3036,24 @@ "failedAddProvider": "添加提供商失败。再试一次。", "connectionError": "连接错误。请再试一次。", "provider": "提供商", - "apiKeyHelp": "__MISSING__:An API key is a password for AI services. Get one from your provider's website (e.g., platform.openai.com, console.anthropic.com).", + "apiKeyHelp": "API 密钥是 AI 服务的密码。请从提供商网站获取(例如 platform.openai.com、console.anthropic.com)。", "tier": { - "subtitle": "__MISSING__:OmniRoute organises providers into three tiers so routing prefers the most reliable, lowest-cost path first.", + "subtitle": "OmniRoute 将提供商组织为三个层级,让路由优先选择最可靠、成本最低的路径。", "tier1": { - "label": "__MISSING__:Premium clients", - "description": "__MISSING__:First-class CLIs with native auth flows and reasoning models." + "label": "高级客户端", + "description": "具备原生认证流程和推理模型的一流 CLI。" }, "tier2": { - "label": "__MISSING__:Cost-optimised", - "description": "__MISSING__:Cheap, high-throughput providers used for everyday traffic." + "label": "成本优化", + "description": "用于日常流量的低成本、高吞吐提供商。" }, "tier3": { - "label": "__MISSING__:Fallback & specialty", - "description": "__MISSING__:Locally-hosted or specialty endpoints used as fallbacks." + "label": "回退与专用", + "description": "作为回退使用的本地托管或专用 Endpoints。" }, - "configure": "__MISSING__:Configure providers" + "configure": "配置提供商" }, - "tierFlowDiagramAlt": "__MISSING__:OmniRoute 3-tier fallback diagram" + "tierFlowDiagramAlt": "OmniRoute 三层回退图" }, "providers": { "title": "提供商", @@ -3061,9 +3061,9 @@ "audioProviders": "音频提供商", "showFreeOnly": "仅显示免费", "addProvider": "添加提供商", - "addFirstProvider": "__MISSING__:Add your first provider", - "addFirstProviderDesc": "__MISSING__:Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts.", - "learnMore": "__MISSING__:Learn more", + "addFirstProvider": "添加你的第一个提供商", + "addFirstProviderDesc": "连接一个 AI 提供商,开始通过 OmniRoute 路由请求。你可以使用免费提供商、API 密钥或 OAuth 账号。", + "learnMore": "了解更多", "editProvider": "编辑提供商", "deleteProvider": "删除提供商", "noProviders": "没有配置提供商", @@ -3096,7 +3096,7 @@ "connected": "{count} 已连接", "errorCount": "{count} 错误 ({code})", "errorCountNoCode": "{count} 错误", - "warningCount": "__MISSING__:{count} Warning", + "warningCount": "{count} 个警告", "noConnections": "无连接", "expiredBadge": "已过期", "expiringSoonBadge": "即将过期", @@ -3233,12 +3233,12 @@ "disableRateLimitProtection": "单击以禁用速率限制保护", "productionKey": "生产密钥", "enterNewApiKey": "输入新的 API 密钥", - "codexApplyModalTitle": "__MISSING__:Apply to Local Codex", - "codexApplyTargetLabel": "__MISSING__:Target path", - "codexApplyBackupLabel": "__MISSING__:Backups", - "codexApplyWarning": "__MISSING__:This will replace the existing auth.json. Continue?", - "codexApplyConfirmCheckbox": "__MISSING__:I confirm I want to replace the existing auth.json", - "codexApply": "__MISSING__:Apply", + "codexApplyModalTitle": "应用到本地 Codex", + "codexApplyTargetLabel": "目标路径", + "codexApplyBackupLabel": "备份", + "codexApplyWarning": "这将替换现有的 auth.json。是否继续?", + "codexApplyConfirmCheckbox": "我确认要替换现有的 auth.json", + "codexApply": "应用", "bulkTabSingle": "单个", "bulkTabBulkAdd": "批量添加", "bulkAddFormatHint": "每行一个密钥。格式:名称|API密钥 或仅 API密钥(按序号自动命名)。", @@ -3398,127 +3398,127 @@ "tokenRefreshFailed": "Token 刷新失败", "applyCodexAuthLocal": "应用认证", "exportCodexAuthFile": "导出认证", - "applyClaudeAuthLocal": "__MISSING__:Apply auth", - "exportClaudeAuthFile": "__MISSING__:Export auth", - "importClaudeAuth": "__MISSING__:Import auth", - "claudeApplyModalTitle": "__MISSING__:Apply to Local Claude Code", - "claudeApplyTargetLabel": "__MISSING__:Target path", - "claudeApplyBackupLabel": "__MISSING__:Backups", - "claudeApplyMcpHint": "__MISSING__:Existing MCP OAuth state will be preserved.", - "claudeApplyWarning": "__MISSING__:This will replace the existing claudeAiOauth section. Continue?", - "claudeApplyConfirmCheckbox": "__MISSING__:I confirm I want to replace the existing claudeAiOauth section", - "claudeApply": "__MISSING__:Apply", - "claudeAuthAppliedLocal": "__MISSING__:Claude auth applied locally", - "claudeAuthApplyFailed": "__MISSING__:Failed to apply Claude auth locally", - "claudeAuthExported": "__MISSING__:Claude auth file exported", - "claudeAuthExportFailed": "__MISSING__:Failed to export Claude auth file", - "claudeImportModalTitle": "__MISSING__:Import Claude Auth", - "claudeImportTabSingle": "__MISSING__:Single", - "claudeImportTabBulk": "__MISSING__:Bulk", - "claudeImportTabUpload": "__MISSING__:Upload file", - "claudeImportTabPaste": "__MISSING__:Paste JSON", - "claudeImportFileLabel": "__MISSING__:Choose .credentials.json", - "claudeImportPasteLabel": "__MISSING__:Paste the JSON content", - "claudeImportEmailLabel": "__MISSING__:Account email", - "claudeImportNameLabel": "__MISSING__:Connection name (optional)", - "claudeImportOverwriteLabel": "__MISSING__:Replace existing connection if account already exists", - "claudeImportSubmit": "__MISSING__:Import", - "claudeImportSuccess": "__MISSING__:Claude connection imported successfully", - "claudeImportInvalidJson": "__MISSING__:Could not parse the file as JSON", - "claudeImportInvalidShape": "__MISSING__:The file is not a valid .credentials.json", - "claudeImportDuplicate": "__MISSING__:Account already exists — enable \"Replace existing\" to overwrite", - "claudeImportIdentityUnverified": "__MISSING__:Bootstrap could not verify the account. Enable \"Replace existing\" or provide an email.", - "claudeImportFailed": "__MISSING__:Failed to import Claude auth", - "claudeImportBulkModeUpload": "__MISSING__:Upload files", - "claudeImportBulkModePaste": "__MISSING__:Paste JSON array", - "claudeImportBulkModeZip": "__MISSING__:Upload ZIP", - "claudeImportBulkUploadHint": "__MISSING__:Drop or pick up to 50 .credentials.json files (256KB each, 10MB total).", - "claudeImportBulkPasteHint": "__MISSING__:Paste an array of objects: [{ json, name?, email? }, ...]", - "claudeImportBulkZipHint": "__MISSING__:ZIP containing .json entries. Max 50 entries, 10MB unpacked.", - "claudeImportBulkSubmit": "__MISSING__:Import all", - "claudeImportBulkSuccess": "__MISSING__:Imported {count} Claude connections", - "claudeImportBulkFailed": "__MISSING__:Some entries failed to import", - "claudeImportBulkZipExtracting": "__MISSING__:Extracting ZIP…", - "claudeImportBulkZipError": "__MISSING__:Failed to extract ZIP", - "applyGeminiAuthLocal": "__MISSING__:Apply auth", - "exportGeminiAuthFile": "__MISSING__:Export auth", - "importGeminiAuth": "__MISSING__:Import auth", - "geminiApplyModalTitle": "__MISSING__:Apply to Local Gemini CLI", - "geminiApplyTargetLabel": "__MISSING__:Target path", - "geminiApplyBackupLabel": "__MISSING__:Backups", - "geminiApplyAccountsHint": "__MISSING__:The google_accounts.json active account will be updated to match this connection.", - "geminiApplyWarning": "__MISSING__:This will replace the existing oauth_creds.json and update google_accounts.json. Continue?", - "geminiApplyConfirmCheckbox": "__MISSING__:I confirm I want to replace the existing oauth_creds.json", - "geminiApply": "__MISSING__:Apply", - "geminiAuthAppliedLocal": "__MISSING__:Gemini auth applied locally", - "geminiAuthApplyFailed": "__MISSING__:Failed to apply Gemini auth locally", - "geminiAuthExported": "__MISSING__:Gemini auth file exported", - "geminiAuthExportFailed": "__MISSING__:Failed to export Gemini auth file", - "geminiImportModalTitle": "__MISSING__:Import Gemini Auth", - "geminiImportTabSingle": "__MISSING__:Single", - "geminiImportTabBulk": "__MISSING__:Bulk", - "geminiImportTabUpload": "__MISSING__:Upload file", - "geminiImportTabPaste": "__MISSING__:Paste JSON", - "geminiImportFileLabel": "__MISSING__:Choose oauth_creds.json", - "geminiImportPasteLabel": "__MISSING__:Paste the JSON content", - "geminiImportEmailLabel": "__MISSING__:Account email", - "geminiImportNameLabel": "__MISSING__:Connection name (optional)", - "geminiImportOverwriteLabel": "__MISSING__:Replace existing connection if account already exists", - "geminiImportSubmit": "__MISSING__:Import", - "geminiImportSuccess": "__MISSING__:Gemini connection imported successfully", - "geminiImportInvalidJson": "__MISSING__:Could not parse the file as JSON", - "geminiImportInvalidShape": "__MISSING__:The file is not a valid oauth_creds.json", - "geminiImportDuplicate": "__MISSING__:Account already exists — enable \"Replace existing\" to overwrite", - "geminiImportIdentityUnverified": "__MISSING__:Could not verify identity from id_token. Enable \"Replace existing\" or provide an email.", - "geminiImportFailed": "__MISSING__:Failed to import Gemini auth", - "geminiImportBulkModeUpload": "__MISSING__:Upload files", - "geminiImportBulkModePaste": "__MISSING__:Paste JSON array", - "geminiImportBulkModeZip": "__MISSING__:Upload ZIP", - "geminiImportBulkUploadHint": "__MISSING__:Drop or pick up to 50 oauth_creds.json files (256KB each, 10MB total).", - "geminiImportBulkPasteHint": "__MISSING__:Paste an array of objects: [{ json, name?, email? }, ...]", - "geminiImportBulkZipHint": "__MISSING__:ZIP containing oauth_creds.json entries. Max 50 entries, 10MB unpacked.", - "geminiImportBulkSubmit": "__MISSING__:Import all", - "geminiImportBulkSuccess": "__MISSING__:Imported {count} Gemini connections", - "geminiImportBulkFailed": "__MISSING__:Some entries failed to import", - "geminiImportBulkZipExtracting": "__MISSING__:Extracting ZIP…", - "geminiImportBulkZipError": "__MISSING__:Failed to extract ZIP", + "applyClaudeAuthLocal": "应用认证", + "exportClaudeAuthFile": "导出认证", + "importClaudeAuth": "导入认证", + "claudeApplyModalTitle": "应用到本地 Claude Code", + "claudeApplyTargetLabel": "目标路径", + "claudeApplyBackupLabel": "备份", + "claudeApplyMcpHint": "现有 MCP OAuth 状态将会保留。", + "claudeApplyWarning": "这将替换现有的 claudeAiOauth 部分。是否继续?", + "claudeApplyConfirmCheckbox": "我确认要替换现有的 claudeAiOauth 部分", + "claudeApply": "应用", + "claudeAuthAppliedLocal": "Claude 认证已应用到本地", + "claudeAuthApplyFailed": "应用 Claude 本地认证失败", + "claudeAuthExported": "Claude 认证文件已导出", + "claudeAuthExportFailed": "导出 Claude 认证文件失败", + "claudeImportModalTitle": "导入 Claude 认证", + "claudeImportTabSingle": "单个", + "claudeImportTabBulk": "批量", + "claudeImportTabUpload": "上传文件", + "claudeImportTabPaste": "粘贴 JSON", + "claudeImportFileLabel": "选择 .credentials.json", + "claudeImportPasteLabel": "粘贴 JSON 内容", + "claudeImportEmailLabel": "账号邮箱", + "claudeImportNameLabel": "连接名称(可选)", + "claudeImportOverwriteLabel": "如果账号已存在,则替换现有连接", + "claudeImportSubmit": "导入", + "claudeImportSuccess": "Claude 连接导入成功", + "claudeImportInvalidJson": "无法将文件解析为 JSON", + "claudeImportInvalidShape": "该文件不是有效的 .credentials.json", + "claudeImportDuplicate": "账号已存在——启用“替换现有连接”以覆盖", + "claudeImportIdentityUnverified": "Bootstrap 无法验证该账号。请启用“替换现有连接”或提供邮箱。", + "claudeImportFailed": "导入 Claude 认证失败", + "claudeImportBulkModeUpload": "上传文件", + "claudeImportBulkModePaste": "粘贴 JSON 数组", + "claudeImportBulkModeZip": "上传 ZIP", + "claudeImportBulkUploadHint": "拖放或选择最多 50 个 .credentials.json 文件(每个 256KB,总计 10MB)。", + "claudeImportBulkPasteHint": "粘贴对象数组:[{ json, name?, email? }, ...]", + "claudeImportBulkZipHint": "包含 .json 条目的 ZIP。最多 50 个条目,解压后不超过 10MB。", + "claudeImportBulkSubmit": "全部导入", + "claudeImportBulkSuccess": "已导入 {count} 个 Claude 连接", + "claudeImportBulkFailed": "部分条目导入失败", + "claudeImportBulkZipExtracting": "正在解压 ZIP…", + "claudeImportBulkZipError": "解压 ZIP 失败", + "applyGeminiAuthLocal": "应用认证", + "exportGeminiAuthFile": "导出认证", + "importGeminiAuth": "导入认证", + "geminiApplyModalTitle": "应用到本地 Gemini CLI", + "geminiApplyTargetLabel": "目标路径", + "geminiApplyBackupLabel": "备份", + "geminiApplyAccountsHint": "google_accounts.json 中的当前活动账号将更新为匹配此连接。", + "geminiApplyWarning": "这将替换现有的 oauth_creds.json 并更新 google_accounts.json。是否继续?", + "geminiApplyConfirmCheckbox": "我确认要替换现有的 oauth_creds.json", + "geminiApply": "应用", + "geminiAuthAppliedLocal": "Gemini 认证已应用到本地", + "geminiAuthApplyFailed": "应用 Gemini 本地认证失败", + "geminiAuthExported": "Gemini 认证文件已导出", + "geminiAuthExportFailed": "导出 Gemini 认证文件失败", + "geminiImportModalTitle": "导入 Gemini 认证", + "geminiImportTabSingle": "单个", + "geminiImportTabBulk": "批量", + "geminiImportTabUpload": "上传文件", + "geminiImportTabPaste": "粘贴 JSON", + "geminiImportFileLabel": "选择 oauth_creds.json", + "geminiImportPasteLabel": "粘贴 JSON 内容", + "geminiImportEmailLabel": "账号邮箱", + "geminiImportNameLabel": "连接名称(可选)", + "geminiImportOverwriteLabel": "如果账号已存在,则替换现有连接", + "geminiImportSubmit": "导入", + "geminiImportSuccess": "Gemini 连接导入成功", + "geminiImportInvalidJson": "无法将文件解析为 JSON", + "geminiImportInvalidShape": "该文件不是有效的 oauth_creds.json", + "geminiImportDuplicate": "账号已存在——启用“替换现有连接”以覆盖", + "geminiImportIdentityUnverified": "无法从 id_token 验证身份。请启用“替换现有连接”或提供邮箱。", + "geminiImportFailed": "导入 Gemini 认证失败", + "geminiImportBulkModeUpload": "上传文件", + "geminiImportBulkModePaste": "粘贴 JSON 数组", + "geminiImportBulkModeZip": "上传 ZIP", + "geminiImportBulkUploadHint": "拖放或选择最多 50 个 oauth_creds.json 文件(每个 256KB,总计 10MB)。", + "geminiImportBulkPasteHint": "粘贴对象数组:[{ json, name?, email? }, ...]", + "geminiImportBulkZipHint": "包含 oauth_creds.json 条目的 ZIP。最多 50 个条目,解压后不超过 10MB。", + "geminiImportBulkSubmit": "全部导入", + "geminiImportBulkSuccess": "已导入 {count} 个 Gemini 连接", + "geminiImportBulkFailed": "部分条目导入失败", + "geminiImportBulkZipExtracting": "正在解压 ZIP…", + "geminiImportBulkZipError": "解压 ZIP 失败", "codexAuthAppliedLocal": "Codex auth.json 已在本地应用", "codexAuthApplyFailed": "在本地应用 Codex auth.json 失败", "codexAuthExported": "Codex auth.json 已导出", "codexAuthExportFailed": "导出 Codex auth.json 失败", - "importCodexAuth": "__MISSING__:Import auth", - "codexImportModalTitle": "__MISSING__:Import Codex Auth", - "codexImportTabSingle": "__MISSING__:Single", - "codexImportTabBulk": "__MISSING__:Bulk", - "codexImportTabUpload": "__MISSING__:Upload file", - "codexImportTabPaste": "__MISSING__:Paste JSON", - "codexImportFileLabel": "__MISSING__:Choose auth.json", - "codexImportFileHint": "__MISSING__:Select the auth.json file exported from Codex or from OmniRoute.", - "codexImportPasteLabel": "__MISSING__:Paste the JSON content", - "codexImportEmailLabel": "__MISSING__:Account email", - "codexImportEmailHint": "__MISSING__:Auto-detected from the file; edit if needed.", - "codexImportNameLabel": "__MISSING__:Connection name (optional)", - "codexImportOverwriteLabel": "__MISSING__:Replace existing connection if account already exists", - "codexImportSubmit": "__MISSING__:Import", - "codexImportSuccess": "__MISSING__:Codex connection imported successfully", - "codexImportInvalidJson": "__MISSING__:Could not parse the file as JSON", - "codexImportInvalidShape": "__MISSING__:The file is not a valid Codex auth.json", - "codexImportDuplicate": "__MISSING__:Account already exists — enable \"Replace existing\" to overwrite", - "codexImportFailed": "__MISSING__:Failed to import Codex auth", - "codexImportDetectedEmail": "__MISSING__:Detected: {email}", - "codexImportNoEmailDetected": "__MISSING__:No email detected in file", - "codexImportBulkModeUpload": "__MISSING__:Upload files", - "codexImportBulkModePaste": "__MISSING__:Paste list", - "codexImportBulkModeZip": "__MISSING__:ZIP archive", - "codexImportBulkUploadHint": "__MISSING__:Select multiple .json files or drag and drop", - "codexImportBulkPasteHint": "__MISSING__:JSON array [ {...}, {...} ] or multiple JSONs separated by --- on its own line", - "codexImportBulkZipHint": "__MISSING__:Upload a .zip containing auth.json files (max 50 files, 10 MB)", - "codexImportBulkSubmit": "__MISSING__:Import {count} accounts", - "codexImportBulkLimit": "__MISSING__:Max 50 files per import", - "codexImportBulkSuccess": "__MISSING__:{count} imported", - "codexImportBulkFailed": "__MISSING__:{count} failed", - "codexImportBulkZipExtracting": "__MISSING__:Extracting ZIP…", - "codexImportBulkZipError": "__MISSING__:Failed to extract ZIP", + "importCodexAuth": "导入认证", + "codexImportModalTitle": "导入 Codex 认证", + "codexImportTabSingle": "单个", + "codexImportTabBulk": "批量", + "codexImportTabUpload": "上传文件", + "codexImportTabPaste": "粘贴 JSON", + "codexImportFileLabel": "选择 auth.json", + "codexImportFileHint": "选择从 Codex 或 OmniRoute 导出的 auth.json 文件。", + "codexImportPasteLabel": "粘贴 JSON 内容", + "codexImportEmailLabel": "账号邮箱", + "codexImportEmailHint": "已从文件自动检测;如有需要可编辑。", + "codexImportNameLabel": "连接名称(可选)", + "codexImportOverwriteLabel": "如果账号已存在,则替换现有连接", + "codexImportSubmit": "导入", + "codexImportSuccess": "Codex 连接导入成功", + "codexImportInvalidJson": "无法将文件解析为 JSON", + "codexImportInvalidShape": "该文件不是有效的 Codex auth.json", + "codexImportDuplicate": "账号已存在——启用“替换现有连接”以覆盖", + "codexImportFailed": "导入 Codex 认证失败", + "codexImportDetectedEmail": "已检测到:{email}", + "codexImportNoEmailDetected": "文件中未检测到邮箱", + "codexImportBulkModeUpload": "上传文件", + "codexImportBulkModePaste": "粘贴列表", + "codexImportBulkModeZip": "ZIP 压缩包", + "codexImportBulkUploadHint": "选择多个 .json 文件或拖放上传", + "codexImportBulkPasteHint": "JSON 数组 [ {...}, {...} ],或多个 JSON,并用单独一行的 --- 分隔", + "codexImportBulkZipHint": "上传包含 auth.json 文件的 .zip(最多 50 个文件,10 MB)", + "codexImportBulkSubmit": "导入 {count} 个账号", + "codexImportBulkLimit": "每次导入最多 50 个文件", + "codexImportBulkSuccess": "已导入 {count} 个", + "codexImportBulkFailed": "{count} 个失败", + "codexImportBulkZipExtracting": "正在解压 ZIP…", + "codexImportBulkZipError": "解压 ZIP 失败", "advancedSettings": "高级设置", "chatPathLabel": "聊天端点路径", "chatPathPlaceholder": "/chat/completions", @@ -3551,14 +3551,14 @@ "apikey": "API key", "audio": "音频", "audioProvidersHeading": "音频 Provider", - "cloudAgentProviders": "__MISSING__:Cloud Agent Providers", + "cloudAgentProviders": "云端 Agent 提供商", "audioShortLabel": "音频", "azureOpenAiBaseUrlHint": "Azure OpenAI 资源的 Base URL。", "bailianBaseUrlHint": "阿里云百炼服务的 Base URL。", "blackboxWebCookieHint": "从 Blackbox Web 会话复制 Cookie。", "blackboxWebCookiePlaceholder": "Blackbox Web Cookie", - "t3ChatWebCookieHint": "__MISSING__:Open t3.chat → DevTools → Application → Local Storage → https://t3.chat, copy 'convex-session-id'. Then open DevTools → Network, copy the full Cookie header from any chat request. Paste both values in the fields below.", - "t3ChatWebCookiePlaceholder": "__MISSING__:convex-session-id=abc123...", + "t3ChatWebCookieHint": "打开 t3.chat → DevTools → Application → Local Storage → https://t3.chat,复制 'convex-session-id'。然后打开 DevTools → Network,从任意聊天请求复制完整 Cookie header。将两个值粘贴到下方字段。", + "t3ChatWebCookiePlaceholder": "convex-session-id=abc123...", "blockClaudeExtraUsageDescription": "隐藏部分 Provider 返回的重复 Claude 额外用量记录,避免和主 token 统计重复。", "blockClaudeExtraUsageLabel": "屏蔽重复 Claude 用量", "bulkPasteAdded": "{count, plural, one {已添加 1 个 key} other {已添加 # 个 key}}", @@ -3618,12 +3618,12 @@ "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", - "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", - "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", - "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", - "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", + "geminiCliProjectIdHint": "你的 Google Cloud Project ID。带例外配置的账号需要此项。输入你的 GCP Project ID 以配合 Gemini CLI 使用。", + "geminiCliProjectIdLabel": "Google Cloud Project ID", + "geminiCliProjectIdPlaceholder": "my-gcp-project-id", + "antigravityProjectIdHint": "Antigravity Cloud Code 请求的可选覆盖项。留空则使用 Google OAuth 期间发现的项目。", + "antigravityProjectIdLabel": "Google Cloud Project ID", + "antigravityProjectIdPlaceholder": "my-gcp-project-id", "grokWebCookieHint": "从 Grok Web 会话复制 Cookie。", "grokWebCookiePlaceholder": "Grok Web Cookie", "herokuBaseUrlHint": "Heroku 部署的 Base URL。", @@ -3717,8 +3717,8 @@ "providerDetailCallbackUrl": "回调 URL", "providerDetailValidClaudeCredentialsFile": "有效的 Claude 凭据文件", "providerDetailPathAutoDetectedAllOs": "路径按操作系统自动检测(Linux/Mac/Windows)。", - "providerDetailMyClaudeAccountPlaceholder": "__MISSING__:My Claude account", - "providerDetailPathAutoDetected": "__MISSING__:Path is auto-detected per OS (Linux/Mac)." + "providerDetailMyClaudeAccountPlaceholder": "我的 Claude 账号", + "providerDetailPathAutoDetected": "路径会根据操作系统自动检测(Linux/Mac)。" }, "settings": { "title": "设置", @@ -3728,9 +3728,9 @@ "routing": "路由", "cache": "缓存", "resilience": "韧性", - "routingSettingsIntro": "__MISSING__:Controls how your requests are routed, transformed, and sent to AI providers.", - "resilienceSettingsIntro": "__MISSING__:Automatic retry, cooldown, and fallback when providers fail.", - "aiSettingsIntro": "__MISSING__:AI-specific settings for thinking budget, model behavior, and compression.", + "routingSettingsIntro": "控制请求如何路由、转换并发送到 AI 提供商。", + "resilienceSettingsIntro": "当提供商失败时自动重试、冷却和回退。", + "aiSettingsIntro": "用于思考预算、模型行为和压缩的 AI 专用设置。", "systemPrompt": "系统提示", "thinkingBudget": "思考预算", "proxy": "代理", @@ -3836,8 +3836,8 @@ "autoDisableDescription": "若提供商连接返回特定的永久封禁信号(如 HTTP 403\"请验证您的账户\"),则将其永久标记为停用。这会将其从组合轮换中移除。", "autoDisableThreshold": "封禁阈值", "autoDisableThresholdDesc": "触发永久停用所需的连续封禁信号次数。", - "resilienceStructureTitle": "__MISSING__:Resilience Structure", - "resilienceStructureDesc": "__MISSING__:This page only configures behavior. Live breaker state is shown on the Health page. Combo-specific retry and round-robin slot control remain on combo settings.", + "resilienceStructureTitle": "韧性结构", + "resilienceStructureDesc": "此页面仅配置行为。实时断路器状态显示在健康页面。组合专属的重试和轮询槽位控制仍在组合设置中。", "enableThinking": "激发思考", "maxThinkingTokens": "最大思考令牌", "enableProxy": "启用代理", @@ -3981,16 +3981,16 @@ "cliFingerprintEnabled": "已有 {count} 个提供商启用了 CLI 指纹", "enableFingerprintTitle": "为 {provider} 启用指纹", "disableFingerprintTitle": "为 {provider} 禁用指纹", - "systemTransforms": "__MISSING__:System-block Transform Pipeline", - "systemTransformsDesc": "__MISSING__:Per-provider ordered pipeline of transforms applied to the request body before forwarding. Supports any provider ID.", - "systemTransformsAddProvider": "__MISSING__:Add provider", - "systemTransformsAddProviderPlaceholder": "__MISSING__:Select a provider…", - "systemTransformsAddProviderAllConfigured": "__MISSING__:All providers already configured", - "systemTransformsRemoveProvider": "__MISSING__:Remove provider", - "systemTransformsNoProviders": "__MISSING__:No providers configured. Add a provider to get started.", - "systemTransformsOpMoveUp": "__MISSING__:Move up", - "systemTransformsOpMoveDown": "__MISSING__:Move down", - "systemTransformsOpDelete": "__MISSING__:Delete op", + "systemTransforms": "System-block 转换管道", + "systemTransformsDesc": "按提供商配置的有序转换管道,会在转发前应用到请求体。支持任意提供商 ID。", + "systemTransformsAddProvider": "添加提供商", + "systemTransformsAddProviderPlaceholder": "选择提供商…", + "systemTransformsAddProviderAllConfigured": "所有提供商均已配置", + "systemTransformsRemoveProvider": "移除提供商", + "systemTransformsNoProviders": "尚未配置提供商。添加一个提供商即可开始。", + "systemTransformsOpMoveUp": "上移", + "systemTransformsOpMoveDown": "下移", + "systemTransformsOpDelete": "删除操作", "routingStrategy": "路由策略", "routingAdvancedGuideTitle": "高级路由指南", "routingAdvancedGuideHint1": "需要可预测优先级时使用 Fill First,需要公平分配时使用 Round Robin,需要延迟弹性时使用 P2C。", @@ -4007,8 +4007,8 @@ "leastUsedDesc": "优先选择最近使用最少的账户", "costOpt": "成本优化", "costOptDesc": "优先选择成本最低的可用账户", - "resetAware": "__MISSING__:Reset-Aware RR", - "resetAwareDesc": "__MISSING__:Prefer accounts with healthy remaining quota and nearer resets", + "resetAware": "重置感知轮询", + "resetAwareDesc": "优先选择剩余额度健康且更快重置的账号", "strictRandom": "严格随机", "strictRandomDesc": "洗牌池模式:每个账户使用一次后再重新洗牌", "stickyLimit": "粘性限制", @@ -4372,197 +4372,197 @@ "compressionModeRtkDesc": "感知命令的工具输出过滤", "compressionModeStacked": "堆叠", "compressionModeStackedDesc": "先进行 RTK 工具输出过滤,再进行 Caveman 消息压缩", - "qdrantTitle": "__MISSING__:Qdrant (Vector memory)", - "qdrantDesc": "__MISSING__:Optional. Indexes semantic memories in an external vector database for faster retrieval.", - "qdrantStatusActive": "__MISSING__:Active", - "qdrantStatusError": "__MISSING__:Error", - "qdrantStatusDisabled": "__MISSING__:Disabled", - "qdrantEnable": "__MISSING__:Enable Qdrant", - "qdrantEnableDesc": "__MISSING__:When enabled, semantic/hybrid strategy can use Qdrant to retrieve memories.", - "qdrantTesting": "__MISSING__:Testing...", - "qdrantTestConnection": "__MISSING__:Test connection", - "qdrantSaved": "__MISSING__:Configuration saved", - "qdrantSaveError": "__MISSING__:Failed to save configuration", - "qdrantHostHint": "__MISSING__:Without port. Example: 127.0.0.1 or http://qdrant", - "qdrantPort": "__MISSING__:Port", - "qdrantPortHint": "__MISSING__:Qdrant default: 6333", - "qdrantCollectionHint": "__MISSING__:Where memory points will be stored.", - "qdrantEmbeddingModel": "__MISSING__:Embedding model", - "qdrantHelpTitle": "__MISSING__:Quick setup help", - "qdrantHelpQuickTitle": "__MISSING__:Quick setup (Qdrant + OpenRouter)", - "qdrantHelpStep1": "__MISSING__:1. Host: Qdrant IP/URL, Port: 6333, Collection: omniroute_memory.", - "qdrantHelpStep2": "__MISSING__:2. If using nvidia/llama-nemotron-embed-vl-1b-v2:free, use collection dimension 2048.", - "qdrantHelpStep3": "__MISSING__:3. Model field: openrouter/nvidia/llama-nemotron-embed-vl-1b-v2:free.", - "qdrantHelpStep4": "__MISSING__:4. Save, test connection, then test search.", - "qdrantEmbeddingQuickSelect": "__MISSING__:Quick select from discovered models...", - "qdrantEmbeddingInputPlaceholder": "__MISSING__:openai/text-embedding-3-small", - "qdrantEmbeddingHint": "__MISSING__:Format: provider/model. The provider credential must be configured.", - "qdrantApiKeyPlaceholderKeep": "__MISSING__:(leave empty to keep current key)", - "qdrantApiKeyPlaceholderOptional": "__MISSING__:(leave empty if not used)", - "qdrantSaveHint": "__MISSING__:Tip: edit host/port/collection/model and click Save. API key is optional.", - "qdrantSearchTestTitle": "__MISSING__:Search test", - "qdrantSearchTestDesc": "__MISSING__:Generates embedding and searches in Qdrant.", - "qdrantSearchPlaceholder": "__MISSING__:Example: user preferences, history, etc", - "qdrantNoResults": "__MISSING__:No results (or Qdrant is not configured).", - "qdrantCleanupTitle": "__MISSING__:Retention and cleanup", - "qdrantCleanupDesc": "__MISSING__:Removes expired and old points based on", - "searching": "__MISSING__:Searching...", - "cleaning": "__MISSING__:Cleaning...", - "cleanNow": "__MISSING__:Clean now", - "optional": "__MISSING__:Optional", - "current": "__MISSING__:Current", - "remove": "__MISSING__:Remove", - "search": "__MISSING__:Search", - "oneproxyTitle": "__MISSING__:1proxy Free Proxy Marketplace", - "oneproxyTotalProxies": "__MISSING__:Total Proxies", - "oneproxyAvgQuality": "__MISSING__:Avg Quality", - "resilienceScope": "__MISSING__:Scope:", - "resilienceTrigger": "__MISSING__:Trigger:", - "resilienceEffect": "__MISSING__:Effect:", - "resilienceRequestQueueTitle": "__MISSING__:Request Queue & Rate", - "resilienceAutoEnableApiKeyProviders": "__MISSING__:Auto-enable for API-key providers", - "resilienceRequestsPerMinute": "__MISSING__:Requests per minute", - "resilienceMinTimeBetweenRequests": "__MISSING__:Minimum time between requests", - "resilienceConcurrentRequests": "__MISSING__:Concurrent requests", - "resilienceMaxQueueWaitTime": "__MISSING__:Maximum queue wait time", - "resilienceBaseCooldown": "__MISSING__:Base cooldown", - "resilienceUseUpstreamRetryHints": "__MISSING__:Use upstream retry hints", - "resilienceDefaultPerProvider": "__MISSING__:Default (per provider)", - "resilienceAlwaysOn": "__MISSING__:Always on", - "resilienceAlwaysOff": "__MISSING__:Always off", - "routingRemoveEntry": "__MISSING__:Remove entry", - "routingNeedlesSubstrings": "__MISSING__:Needles (substrings to match)", - "routingCaseSensitive": "__MISSING__:Case sensitive", - "routingPrefixes": "__MISSING__:Prefixes", - "routingMatch": "__MISSING__:Match", - "routingReplacement": "__MISSING__:Replacement", - "routingReplaceAllOccurrences": "__MISSING__:Replace all occurrences", - "routingPatternRegex": "__MISSING__:Pattern (regex)", - "routingFlags": "__MISSING__:Flags", - "routingNeedles": "__MISSING__:Needles", - "routingBlockText": "__MISSING__:Block text", - "routingIdempotencyKey": "__MISSING__:Idempotency key", - "routingEntrypoint": "__MISSING__:Entrypoint", - "routingVersionFormat": "__MISSING__:Version format", - "routingCchAlgorithm": "__MISSING__:CCH algorithm", - "routingWordsToObfuscate": "__MISSING__:Words to obfuscate (ZWJ inserted after first char)", - "logsSettingsTitle": "__MISSING__:Logs Settings", - "detailedLogsLabel": "__MISSING__:Detailed Logs Enabled", - "detailedLogsDesc": "__MISSING__:Enable detailed request/response logging", - "callLogPipelineLabel": "__MISSING__:Call Log Pipeline", - "callLogPipelineDesc": "__MISSING__:Enable call log processing pipeline", - "maxDetailSizeLabel": "__MISSING__:Max Detail Size (KB)", - "maxDetailSizeDesc": "__MISSING__:Maximum size for detailed log entries", - "ringBufferSizeLabel": "__MISSING__:Ring Buffer Size", - "ringBufferSizeDesc": "__MISSING__:Size of the ring buffer for logs", - "semanticCacheEnabledLabel": "__MISSING__:Semantic Cache Enabled", - "semanticCacheMaxSizeLabel": "__MISSING__:Semantic Cache Max Size", - "semanticCacheMaxSizeDesc": "__MISSING__:Maximum number of semantic cache entries", - "semanticCacheTTLLabel": "__MISSING__:Semantic Cache TTL", - "promptCacheEnabledLabel": "__MISSING__:Prompt Cache Enabled", - "promptCacheEnabledDesc": "__MISSING__:Enable prompt caching", - "promptCacheStrategyLabel": "__MISSING__:Prompt Cache Strategy", - "promptCacheStrategyDesc": "__MISSING__:Strategy for prompt caching", - "alwaysPreserveClientCacheLabel": "__MISSING__:Always Preserve Client Cache", - "alwaysPreserveClientCacheDesc": "__MISSING__:Client cache preservation policy", - "logRetentionPolicyTitle": "__MISSING__:Log retention policy", - "resilienceUseUpstream429HintsForBreaker": "__MISSING__:Use upstream 429 hints (breaker)", - "appearanceLogoPreviewAlt": "__MISSING__:Logo preview", - "appearanceFaviconPreviewAlt": "__MISSING__:Favicon preview", - "oneproxyLastSync": "__MISSING__:Last Sync", - "oneproxyAllProtocols": "__MISSING__:All Protocols", - "oneproxyCountryCodePlaceholder": "__MISSING__:Country code (e.g. US)", - "oneproxyMinQualityPlaceholder": "__MISSING__:Min quality", - "oneproxyLoadingProxies": "__MISSING__:Loading proxies...", - "oneproxyLastSyncLabel": "__MISSING__:Last sync:", - "oneproxyProxiesFetched": "__MISSING__:Proxies fetched:", - "oneproxyConsecutiveFailures": "__MISSING__:Consecutive failures:", - "oneproxyErrorLabel": "__MISSING__:Error:", - "oneproxyNever": "__MISSING__:Never", - "oneproxySyncStatusTitle": "__MISSING__:Sync Status", - "oneproxySuccess": "__MISSING__:Success", - "oneproxyFailed": "__MISSING__:Failed", - "routingAntigravitySignatureTitle": "__MISSING__:Antigravity Signature Cache Mode", - "routingHeaderFingerprintTitle": "__MISSING__:Header fingerprint (per provider)", - "routingServerRejectedSave": "__MISSING__:⚠ Server rejected save:", - "routingAddTransformOp": "__MISSING__:Add a transform op", - "routingClientCacheControlTitle": "__MISSING__:Client Cache Control", - "visionBridge": "__MISSING__:Vision Bridge", - "routingZeroConfigTitle": "__MISSING__:Zero-Config Auto-Routing", - "routingDefaultAutoVariant": "__MISSING__:Default Auto Variant", - "visionBridgeModel": "__MISSING__:Bridge Model", - "resilienceMaxBackoffSteps": "__MISSING__:Max backoff steps", - "resilienceBaseCooldownLabel": "__MISSING__:Base cooldown", - "resilienceUseUpstreamRetryHintsLabel": "__MISSING__:Use upstream retry hints", - "resilienceYes": "__MISSING__:Yes", - "resilienceNo": "__MISSING__:No", - "resilienceUseUpstream429BreakerLabel": "__MISSING__:Use upstream 429 hints (breaker)", - "resilienceDefault": "__MISSING__:Default", - "resilienceMaxBackoffStepsLabel": "__MISSING__:Max backoff steps", - "visionBridgePrompt": "__MISSING__:Bridge Prompt", - "visionBridgeTimeoutMs": "__MISSING__:Timeout (ms)", - "resilienceConnectionCooldownTitle": "__MISSING__:Connection Cooldown", - "visionBridgeMaxImagesPerRequest": "__MISSING__:Max Images Per Request", - "resilienceFailureThreshold": "__MISSING__:Failure threshold", - "resilienceResetTimeout": "__MISSING__:Reset timeout", - "resilienceFailureThresholdLabel": "__MISSING__:Failure threshold", - "resilienceResetTimeoutLabel": "__MISSING__:Reset timeout", - "visionBridgeModelPlaceholder": "__MISSING__:openai/gpt-4o-mini", - "visionBridgePromptPlaceholder": "__MISSING__:Describe this image concisely.", - "resilienceProviderBreakerTitle": "__MISSING__:Circuit Breaker per Provider", - "storageDatabaseBackupRetention": "__MISSING__:Database backup retention", - "storagePurgeData": "__MISSING__:Purge Data", - "retentionQuotaSnapshots": "__MISSING__:Quota Snapshots (days)", - "retentionMcpAudit": "__MISSING__:MCP Audit (days)", - "retentionA2aEvents": "__MISSING__:A2A Events (days)", - "retentionCallLogs": "__MISSING__:Call Logs (days)", - "retentionUsageHistory": "__MISSING__:Usage History (days)", - "retentionMemoryEntries": "__MISSING__:Memory Entries (days)", - "storageAutoVacuumMode": "__MISSING__:Auto Vacuum Mode", - "storageScheduledVacuum": "__MISSING__:Scheduled Vacuum", - "storageVacuumHour": "__MISSING__:Vacuum Hour (0-23)", - "storagePageSize": "__MISSING__:Page Size (bytes)", - "storageDatabaseSize": "__MISSING__:Database Size", - "storagePageCount": "__MISSING__:Page Count", - "storageFreelistCount": "__MISSING__:Freelist Count", - "storageLastVacuum": "__MISSING__:Last Vacuum", - "storageLastOptimization": "__MISSING__:Last Optimization", - "storageIntegrityCheck": "__MISSING__:Integrity Check", - "storageIntegrityOk": "__MISSING__:✓ OK", - "storageIntegrityError": "__MISSING__:✗ Error", - "storageUsageTokenBuffer": "__MISSING__:Usage Token Buffer", - "compressionSettingsAutoTriggerMode": "__MISSING__:Auto trigger mode", - "compressionSettingsMcpDescriptionCompression": "__MISSING__:MCP description compression", - "compressionSettingsCavemanIntensity": "__MISSING__:Caveman intensity", - "compressionSettingsCavemanOutputMode": "__MISSING__:Caveman output mode", - "compressionSettingsOutputIntensity": "__MISSING__:Output intensity", - "compressionSettingsAutoClarityBypass": "__MISSING__:Auto clarity bypass", - "resilienceWaitForCooldown": "__MISSING__:Wait for Cooldown", - "resilienceEnableServerSideWait": "__MISSING__:Enable server-side wait", - "resilienceMaximumRetries": "__MISSING__:Maximum retries", - "resilienceMaximumWaitPerRetry": "__MISSING__:Maximum wait per retry", - "memorySkillsSkillsmpMarketplace": "__MISSING__:SkillsMP Marketplace", - "memorySkillsFailedToSave": "__MISSING__:Failed to save", - "memorySkillsApiKey": "__MISSING__:API Key", - "memorySkillsActiveSkillsProvider": "__MISSING__:Active Skills Provider", - "cliproxyapiFallback": "__MISSING__:CLIProxyAPI Fallback", - "cliproxyapiEnableFallback": "__MISSING__:Enable CLIProxyAPI Fallback", - "cliproxyapiUrl": "__MISSING__:CLIProxyAPI URL", - "cliproxyapiStatus": "__MISSING__:CLIProxyAPI Status", - "cliproxyapiNotDetected": "__MISSING__:Not detected", - "payloadRulesTitle": "__MISSING__:Payload Rules", - "modelCooldownsTitle": "__MISSING__:Models in cooldown", - "modelCooldownsEmpty": "__MISSING__:No models in cooldown right now.", - "codexFastTierTitle": "__MISSING__:Codex Fast Tier", - "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", - "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", - "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", - "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", - "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", - "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", - "claudeFastModeModelsLabel": "__MISSING__:Applied to models ({count})", - "claudeFastModeModelCheckbox": "__MISSING__:Enable Fast Mode for {model}", - "claudeFastModeSaveError": "__MISSING__:Failed to update Claude Fast Mode setting" + "qdrantTitle": "Qdrant(向量记忆)", + "qdrantDesc": "可选。将语义记忆索引到外部向量数据库,以便更快检索。", + "qdrantStatusActive": "活跃", + "qdrantStatusError": "错误", + "qdrantStatusDisabled": "已禁用", + "qdrantEnable": "启用 Qdrant", + "qdrantEnableDesc": "启用后,语义/混合策略可以使用 Qdrant 检索记忆。", + "qdrantTesting": "正在测试...", + "qdrantTestConnection": "测试连接", + "qdrantSaved": "配置已保存", + "qdrantSaveError": "保存配置失败", + "qdrantHostHint": "不含端口。例如:127.0.0.1 或 http://qdrant", + "qdrantPort": "端口", + "qdrantPortHint": "Qdrant 默认值:6333", + "qdrantCollectionHint": "记忆点的存储位置。", + "qdrantEmbeddingModel": "嵌入模型", + "qdrantHelpTitle": "快速设置帮助", + "qdrantHelpQuickTitle": "快速设置(Qdrant + OpenRouter)", + "qdrantHelpStep1": "1. Host:Qdrant IP/URL,Port:6333,Collection:omniroute_memory。", + "qdrantHelpStep2": "2. 如果使用 nvidia/llama-nemotron-embed-vl-1b-v2:free,请使用集合维度 2048。", + "qdrantHelpStep3": "3. Model 字段:openrouter/nvidia/llama-nemotron-embed-vl-1b-v2:free。", + "qdrantHelpStep4": "4. 保存、测试连接,然后测试搜索。", + "qdrantEmbeddingQuickSelect": "从已发现模型中快速选择...", + "qdrantEmbeddingInputPlaceholder": "openai/text-embedding-3-small", + "qdrantEmbeddingHint": "格式:provider/model。必须已配置该提供商凭据。", + "qdrantApiKeyPlaceholderKeep": "(留空以保留当前密钥)", + "qdrantApiKeyPlaceholderOptional": "(不使用则留空)", + "qdrantSaveHint": "提示:编辑 host/port/collection/model,然后点击保存。API 密钥为可选。", + "qdrantSearchTestTitle": "搜索测试", + "qdrantSearchTestDesc": "生成嵌入并在 Qdrant 中搜索。", + "qdrantSearchPlaceholder": "例如:用户偏好、历史等", + "qdrantNoResults": "无结果(或 Qdrant 未配置)。", + "qdrantCleanupTitle": "保留与清理", + "qdrantCleanupDesc": "根据以下条件移除过期和旧的点", + "searching": "正在搜索...", + "cleaning": "正在清理...", + "cleanNow": "立即清理", + "optional": "可选", + "current": "当前", + "remove": "移除", + "search": "搜索", + "oneproxyTitle": "1proxy 免费代理市场", + "oneproxyTotalProxies": "代理总数", + "oneproxyAvgQuality": "平均质量", + "resilienceScope": "范围:", + "resilienceTrigger": "触发:", + "resilienceEffect": "效果:", + "resilienceRequestQueueTitle": "请求队列与速率", + "resilienceAutoEnableApiKeyProviders": "为 API 密钥提供商自动启用", + "resilienceRequestsPerMinute": "每分钟请求数", + "resilienceMinTimeBetweenRequests": "请求之间的最小间隔", + "resilienceConcurrentRequests": "并发请求数", + "resilienceMaxQueueWaitTime": "最大队列等待时间", + "resilienceBaseCooldown": "基础冷却时间", + "resilienceUseUpstreamRetryHints": "使用上游重试提示", + "resilienceDefaultPerProvider": "默认(按提供商)", + "resilienceAlwaysOn": "始终开启", + "resilienceAlwaysOff": "始终关闭", + "routingRemoveEntry": "移除条目", + "routingNeedlesSubstrings": "Needles(要匹配的子字符串)", + "routingCaseSensitive": "区分大小写", + "routingPrefixes": "前缀", + "routingMatch": "匹配", + "routingReplacement": "替换内容", + "routingReplaceAllOccurrences": "替换所有出现项", + "routingPatternRegex": "模式(正则)", + "routingFlags": "标志", + "routingNeedles": "Needles", + "routingBlockText": "块文本", + "routingIdempotencyKey": "幂等键", + "routingEntrypoint": "入口点", + "routingVersionFormat": "版本格式", + "routingCchAlgorithm": "CCH 算法", + "routingWordsToObfuscate": "要混淆的词(在首字符后插入 ZWJ)", + "logsSettingsTitle": "日志设置", + "detailedLogsLabel": "启用详细日志", + "detailedLogsDesc": "启用详细请求/响应日志", + "callLogPipelineLabel": "调用日志管道", + "callLogPipelineDesc": "启用调用日志处理管道", + "maxDetailSizeLabel": "最大详情大小(KB)", + "maxDetailSizeDesc": "详细日志条目的最大大小", + "ringBufferSizeLabel": "环形缓冲区大小", + "ringBufferSizeDesc": "日志环形缓冲区大小", + "semanticCacheEnabledLabel": "启用语义缓存", + "semanticCacheMaxSizeLabel": "语义缓存最大大小", + "semanticCacheMaxSizeDesc": "语义缓存条目的最大数量", + "semanticCacheTTLLabel": "语义缓存 TTL", + "promptCacheEnabledLabel": "启用 Prompt 缓存", + "promptCacheEnabledDesc": "启用 Prompt 缓存", + "promptCacheStrategyLabel": "Prompt 缓存策略", + "promptCacheStrategyDesc": "Prompt 缓存策略", + "alwaysPreserveClientCacheLabel": "始终保留客户端缓存", + "alwaysPreserveClientCacheDesc": "客户端缓存保留策略", + "logRetentionPolicyTitle": "日志保留策略", + "resilienceUseUpstream429HintsForBreaker": "使用上游 429 提示(断路器)", + "appearanceLogoPreviewAlt": "Logo 预览", + "appearanceFaviconPreviewAlt": "Favicon 预览", + "oneproxyLastSync": "上次同步", + "oneproxyAllProtocols": "所有协议", + "oneproxyCountryCodePlaceholder": "国家代码(例如 US)", + "oneproxyMinQualityPlaceholder": "最低质量", + "oneproxyLoadingProxies": "正在加载代理...", + "oneproxyLastSyncLabel": "上次同步:", + "oneproxyProxiesFetched": "已获取代理:", + "oneproxyConsecutiveFailures": "连续失败次数:", + "oneproxyErrorLabel": "错误:", + "oneproxyNever": "从未", + "oneproxySyncStatusTitle": "同步状态", + "oneproxySuccess": "成功", + "oneproxyFailed": "失败", + "routingAntigravitySignatureTitle": "Antigravity 签名缓存模式", + "routingHeaderFingerprintTitle": "Header 指纹(按提供商)", + "routingServerRejectedSave": "⚠ 服务器拒绝保存:", + "routingAddTransformOp": "添加转换操作", + "routingClientCacheControlTitle": "客户端缓存控制", + "visionBridge": "视觉桥接", + "routingZeroConfigTitle": "零配置自动路由", + "routingDefaultAutoVariant": "默认 Auto 变体", + "visionBridgeModel": "桥接模型", + "resilienceMaxBackoffSteps": "最大退避步数", + "resilienceBaseCooldownLabel": "基础冷却时间", + "resilienceUseUpstreamRetryHintsLabel": "使用上游重试提示", + "resilienceYes": "是", + "resilienceNo": "否", + "resilienceUseUpstream429BreakerLabel": "使用上游 429 提示(断路器)", + "resilienceDefault": "默认", + "resilienceMaxBackoffStepsLabel": "最大退避步数", + "visionBridgePrompt": "桥接 Prompt", + "visionBridgeTimeoutMs": "超时(毫秒)", + "resilienceConnectionCooldownTitle": "连接冷却", + "visionBridgeMaxImagesPerRequest": "每个请求最大图片数", + "resilienceFailureThreshold": "失败阈值", + "resilienceResetTimeout": "重置超时", + "resilienceFailureThresholdLabel": "失败阈值", + "resilienceResetTimeoutLabel": "重置超时", + "visionBridgeModelPlaceholder": "openai/gpt-4o-mini", + "visionBridgePromptPlaceholder": "简要描述这张图片。", + "resilienceProviderBreakerTitle": "按提供商设置断路器", + "storageDatabaseBackupRetention": "数据库备份保留", + "storagePurgeData": "清除数据", + "retentionQuotaSnapshots": "额度快照(天)", + "retentionMcpAudit": "MCP 审计(天)", + "retentionA2aEvents": "A2A 事件(天)", + "retentionCallLogs": "调用日志(天)", + "retentionUsageHistory": "使用历史(天)", + "retentionMemoryEntries": "记忆条目(天)", + "storageAutoVacuumMode": "自动 Vacuum 模式", + "storageScheduledVacuum": "定时 Vacuum", + "storageVacuumHour": "Vacuum 小时(0-23)", + "storagePageSize": "页大小(字节)", + "storageDatabaseSize": "数据库大小", + "storagePageCount": "页数", + "storageFreelistCount": "空闲列表数量", + "storageLastVacuum": "上次 Vacuum", + "storageLastOptimization": "上次优化", + "storageIntegrityCheck": "完整性检查", + "storageIntegrityOk": "✓ 正常", + "storageIntegrityError": "✗ 错误", + "storageUsageTokenBuffer": "使用量 Token 缓冲", + "compressionSettingsAutoTriggerMode": "自动触发模式", + "compressionSettingsMcpDescriptionCompression": "MCP 描述压缩", + "compressionSettingsCavemanIntensity": "Caveman 强度", + "compressionSettingsCavemanOutputMode": "Caveman 输出模式", + "compressionSettingsOutputIntensity": "输出强度", + "compressionSettingsAutoClarityBypass": "自动清晰度绕过", + "resilienceWaitForCooldown": "等待冷却", + "resilienceEnableServerSideWait": "启用服务端等待", + "resilienceMaximumRetries": "最大重试次数", + "resilienceMaximumWaitPerRetry": "每次重试最大等待时间", + "memorySkillsSkillsmpMarketplace": "SkillsMP 市场", + "memorySkillsFailedToSave": "保存失败", + "memorySkillsApiKey": "API 密钥", + "memorySkillsActiveSkillsProvider": "当前 Skills 提供商", + "cliproxyapiFallback": "CLIProxyAPI 回退", + "cliproxyapiEnableFallback": "启用 CLIProxyAPI 回退", + "cliproxyapiUrl": "CLIProxyAPI URL", + "cliproxyapiStatus": "CLIProxyAPI 状态", + "cliproxyapiNotDetected": "未检测到", + "payloadRulesTitle": "Payload 规则", + "modelCooldownsTitle": "冷却中的模型", + "modelCooldownsEmpty": "当前没有处于冷却中的模型。", + "codexFastTierTitle": "Codex 快速层级", + "codexFastTierDesc": "为 OpenAI Codex 请求全局注入 service_tier=priority。", + "codexFastTierHint": "启用后,OmniRoute 会为尚未指定层级的连接,在出站 Codex 请求中添加 service_tier=priority。Priority 层级需要 OpenAI Enterprise API 密钥或 ChatGPT-auth Codex 路径;其他密钥类型会收到 OpenAI 返回的层级相关错误。Codex 提供商页面中的单连接设置优先级更高。", + "codexFastTierSaveError": "更新 Codex 快速层级设置失败", + "claudeFastModeTitle": "Claude Fast Mode", + "claudeFastModeDesc": "将选定的 Claude 请求加入 Anthropic Fast Mode(speed:\"fast\")。", + "claudeFastModeHint": "Anthropic 官方并不支持 SDK 风格客户端使用 Fast Mode。启用后,OmniRoute 会转发 X-CPA-Force-Fast-Mode header,让配套的 CLIProxyAPI 构建可以选择性模拟入口点。只有列出的 Opus 模型受 Anthropic 客户端侧检查限制。订阅层级、Max 套餐和 Fast Mode 点数余额仍会在服务端强制执行——即使开关已开启,Anthropic 也可能返回 out_of_credits。", + "claudeFastModeModelsLabel": "应用到模型({count})", + "claudeFastModeModelCheckbox": "为 {model} 启用 Fast Mode", + "claudeFastModeSaveError": "更新 Claude Fast Mode 设置失败" }, "contextRtk": { "title": "RTK 引擎", @@ -4575,17 +4575,17 @@ "toolResults": "工具结果", "assistantMessages": "助手消息", "codeBlocks": "代码块", - "filterCatalog": "__MISSING__:Filter Catalog", - "filterCatalogDesc": "__MISSING__:Available output filters by category", - "guidedConfig": "__MISSING__:Configuration", - "guidedConfigDesc": "__MISSING__:Adjust how RTK filters your terminal output", + "filterCatalog": "过滤器目录", + "filterCatalogDesc": "按类别显示可用输出过滤器", + "guidedConfig": "配置", + "guidedConfigDesc": "调整 RTK 如何过滤终端输出", "maxLines": "最大行数", "maxChars": "最大字符数", "deduplicateThreshold": "去重阈值", "customFilters": "自定义过滤器", - "detectedType": "__MISSING__:Detected type", - "confidence": "__MISSING__:Confidence", - "beforeAfter": "__MISSING__:Before / After", + "detectedType": "检测到的类型", + "confidence": "置信度", + "beforeAfter": "前 / 后", "trustProjectFilters": "信任项目过滤器", "rawOutputRetention": "原始输出保留", "rawOutputNever": "从不", @@ -4594,11 +4594,11 @@ "rawOutputMaxBytes": "原始输出最大字节数", "filterTesting": "过滤测试", "pasteOutput": "粘贴工具输出以测试过滤...", - "presetHigh": "__MISSING__:Aggressive", - "presetLow": "__MISSING__:Light", - "presetMaxChars": "__MISSING__:Max output characters", - "presetMaxLines": "__MISSING__:Max output lines", - "presetMedium": "__MISSING__:Standard", + "presetHigh": "激进", + "presetLow": "轻量", + "presetMaxChars": "最大输出字符数", + "presetMaxLines": "最大输出行数", + "presetMedium": "标准", "run": "运行", "result": "结果", "previewEmpty": "运行示例以预览 RTK 输出。", @@ -4607,12 +4607,12 @@ "filtersActive": "活动过滤器", "requests": "请求数", "avgSavings": "平均节省", - "simpleMode": "__MISSING__:Simple", - "advancedMode": "__MISSING__:Advanced", - "searchFilters": "__MISSING__:Search filters...", - "tooltipDedup": "__MISSING__:How aggressively to remove repeated lines.", - "tooltipMaxChars": "__MISSING__:Maximum characters per output block.", - "tooltipMaxLines": "__MISSING__:Maximum lines to keep from command output. Excess is truncated." + "simpleMode": "简单", + "advancedMode": "高级", + "searchFilters": "搜索过滤器...", + "tooltipDedup": "移除重复行的激进程度。", + "tooltipMaxChars": "每个输出块的最大字符数。", + "tooltipMaxLines": "命令输出保留的最大行数。超出部分会被截断。" }, "contextCombos": { "title": "压缩组合", @@ -4639,43 +4639,43 @@ "contextCaveman": { "title": "Caveman 引擎", "description": "基于规则的消息压缩,包含语言包、分析和输出模式控制。", - "advancedConfig": "__MISSING__:Advanced Configuration", - "advancedConfigDesc": "__MISSING__:Fine-tune compression behavior", - "aggressiveSettings": "__MISSING__:Aggressive Settings", - "aggressiveSettingsDesc": "__MISSING__:Maximum compression with potential quality trade-offs", + "advancedConfig": "高级配置", + "advancedConfigDesc": "微调压缩行为", + "aggressiveSettings": "激进设置", + "aggressiveSettingsDesc": "最大压缩,可能牺牲一定质量", "requests": "请求数", "tokensSaved": "已节省 token", "savingsPercent": "节省比例", "avgLatency": "平均延迟", "languagePacks": "语言包", "languagePacksDesc": "为特定语言启用压缩规则。", - "labelAutoTrigger": "__MISSING__:Auto-compress when context exceeds", - "labelCompressionRate": "__MISSING__:Compression strength", - "labelMaxTokens": "__MISSING__:Target tokens per message", - "labelMinLength": "__MISSING__:Minimum message length", - "labelMinSavings": "__MISSING__:Minimum tokens to save", + "labelAutoTrigger": "当上下文超过时自动压缩", + "labelCompressionRate": "压缩强度", + "labelMaxTokens": "每条消息的目标 Tokens", + "labelMinLength": "最小消息长度", + "labelMinSavings": "最少节省 Tokens", "enabled": "已启用", "autoDetect": "自动检测语言", "rulesCount": "{count} 条规则", "analyticsTitle": "压缩分析", "noAnalytics": "尚无压缩分析。", - "outputMode": "__MISSING__:Output Mode", + "outputMode": "输出模式", "outputModeDesc": "指示 LLM 以简洁、紧凑的格式回复。", "outputModeTitle": "输出模式", - "quickSettings": "__MISSING__:Quick Settings", - "quickSettingsDesc": "__MISSING__:Basic compression settings to get started", + "quickSettings": "快速设置", + "quickSettingsDesc": "用于快速开始的基础压缩设置", "autoClarity": "自动清晰度绕过", "bypassConditions": "绕过条件", "bypassConditionsList": "安全、不可逆、需澄清、顺序敏感", - "simpleMode": "__MISSING__:Simple", - "advancedMode": "__MISSING__:Advanced", - "tooltipAutoTrigger": "__MISSING__:Automatically compress when context exceeds this size.", - "tooltipCompressionRate": "__MISSING__:0.0 = none, 1.0 = maximum. 0.5 = balanced.", - "tooltipMaxTokens": "__MISSING__:Target token count after compression. Lower = more aggressive.", - "tooltipMinLength": "__MISSING__:Messages shorter than this are not compressed. Lower = more compression.", - "tooltipMinSavings": "__MISSING__:Only compress if it saves at least this many tokens.", - "ultraSettings": "__MISSING__:Ultra Settings", - "ultraSettingsDesc": "__MISSING__:SLM-powered semantic compression", + "simpleMode": "简单", + "advancedMode": "高级", + "tooltipAutoTrigger": "当上下文超过此大小时自动压缩。", + "tooltipCompressionRate": "0.0 = 不压缩,1.0 = 最大压缩。0.5 = 平衡。", + "tooltipMaxTokens": "压缩后的目标 Token 数。越低越激进。", + "tooltipMinLength": "短于此长度的消息不会被压缩。越低压缩越多。", + "tooltipMinSavings": "只有至少节省这么多 Tokens 时才压缩。", + "ultraSettings": "Ultra 设置", + "ultraSettingsDesc": "由 SLM 驱动的语义压缩", "preview": { "lite": "回复要简洁。保留技术术语、代码、错误、URL 和标识符。", "full": "回复要简短紧凑。保留所有技术实质。", @@ -4780,8 +4780,8 @@ "thinking": "思考", "system-prompt": "系统提示", "streaming": "流媒体", - "vision": "__MISSING__:Vision", - "schema-coercion": "__MISSING__:Schema Coercion" + "vision": "视觉", + "schema-coercion": "Schema 强制转换" }, "templateDescriptions": { "simple-chat": "基本短信", @@ -4790,8 +4790,8 @@ "thinking": "扩展思维/推理", "system-prompt": "复杂系统指令", "streaming": "SSE 流请求", - "vision": "__MISSING__:Multimodal request with image input", - "schema-coercion": "__MISSING__:Structured-output / JSON schema enforcement" + "vision": "包含图像输入的多模态请求", + "schema-coercion": "结构化输出 / JSON schema 强制约束" }, "templatePayloads": { "simpleChat": { @@ -4820,14 +4820,14 @@ "prompt": "给我讲一个关于机器人学习绘画的小故事。" }, "vision": { - "system": "__MISSING__:You are an assistant that describes images precisely.", - "userPrompt": "__MISSING__:What is shown in this image?", - "imageUrl": "__MISSING__:https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/600px-PNG_transparency_demonstration_1.png" + "system": "你是一个精确描述图像的助手。", + "userPrompt": "这张图片中显示了什么?", + "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/600px-PNG_transparency_demonstration_1.png" }, "schemaCoercion": { - "userPrompt": "__MISSING__:Look up the weather for Tokyo using metric units and include the hourly breakdown.", - "toolDescription": "__MISSING__:Fetch weather for a city with structured options.", - "cityDescription": "__MISSING__:The city to query, e.g. 'Tokyo' or 'New York'." + "userPrompt": "使用公制单位查询东京天气,并包含逐小时明细。", + "toolDescription": "使用结构化选项获取城市天气。", + "cityDescription": "要查询的城市,例如 'Tokyo' 或 'New York'。" } }, "openaiCompatibleLabel": "OpenAI 兼容", @@ -4874,33 +4874,33 @@ "eventSourceMainPipeline": "• 主请求流水线(CLI/IDE/API 流量)", "liveMonitorDescriptionPrefix": "这里会显示 API 调用流经 OmniRoute 时产生的翻译事件。事件来自内存缓冲区(重启后会重置)。使用", "liveMonitorDescriptionSuffix": ",或外部 API 调用来生成事件。", - "streamTransformer": "__MISSING__:Stream Transformer", - "modeDescriptionStreamTransformer": "__MISSING__:Run chat completions SSE streams through the Responses transformer.", - "streamTransformerTitle": "__MISSING__:Responses Stream Transformer", - "streamTransformerDescription": "__MISSING__:Paste a chat completions SSE stream, run it through OmniRoute's Responses transformer, and inspect the emitted response.* events before wiring a client.", - "loadTextSample": "__MISSING__:Load text sample", - "loadToolSample": "__MISSING__:Load tool-call sample", - "transformToResponses": "__MISSING__:Transform to Responses", - "rawChatSseInput": "__MISSING__:Raw chat completions SSE", - "transformedResponsesSse": "__MISSING__:Transformed Responses API SSE", - "noResultsYet": "__MISSING__:No results yet", - "transformedEvents": "__MISSING__:Transformed events", - "uniqueEventTypes": "__MISSING__:Unique event types", - "inputLines": "__MISSING__:Input lines", - "outputLines": "__MISSING__:Output lines", - "transformedEventTimeline": "__MISSING__:Transformed event timeline", - "transformerTimelineHint": "__MISSING__:Run the transformer to inspect emitted response.output_* events in order.", - "eventType": "__MISSING__:Event type", - "eventPreview": "__MISSING__:Preview", - "comboRouted": "__MISSING__:Combo routed", - "uniqueEndpoints": "__MISSING__:Unique endpoints", - "routeDetails": "__MISSING__:Route details", - "comboBadge": "__MISSING__:Combo", - "routeEndpointLabel": "__MISSING__:Endpoint", - "routeConnectionLabel": "__MISSING__:Connection", - "scenarioVision": "__MISSING__:Vision (image understanding)", - "scenarioSchemaCoercion": "__MISSING__:Schema coercion (structured output)", - "techniques": "__MISSING__:Techniques:" + "streamTransformer": "流转换器", + "modeDescriptionStreamTransformer": "通过 Responses 转换器运行 chat completions SSE 流。", + "streamTransformerTitle": "Responses 流转换器", + "streamTransformerDescription": "粘贴 chat completions SSE 流,通过 OmniRoute 的 Responses 转换器运行,并在接入客户端前检查发出的 response.* 事件。", + "loadTextSample": "加载文本示例", + "loadToolSample": "加载工具调用示例", + "transformToResponses": "转换为 Responses", + "rawChatSseInput": "原始 chat completions SSE", + "transformedResponsesSse": "转换后的 Responses API SSE", + "noResultsYet": "暂无结果", + "transformedEvents": "已转换事件", + "uniqueEventTypes": "唯一事件类型", + "inputLines": "输入行数", + "outputLines": "输出行数", + "transformedEventTimeline": "已转换事件时间线", + "transformerTimelineHint": "运行转换器以按顺序检查发出的 response.output_* 事件。", + "eventType": "事件类型", + "eventPreview": "预览", + "comboRouted": "已路由到组合", + "uniqueEndpoints": "唯一 Endpoint", + "routeDetails": "路由详情", + "comboBadge": "组合", + "routeEndpointLabel": "Endpoint", + "routeConnectionLabel": "连接", + "scenarioVision": "视觉(图像理解)", + "scenarioSchemaCoercion": "Schema 强制转换(结构化输出)", + "techniques": "技术:" }, "usage": { "title": "用量", @@ -5166,45 +5166,45 @@ "suiteBuilderDescriptionLabel": "描述", "targetComparisonHint": "在目标之间并排比较评估结果。", "compareCompletedWithScore": "对比已完成 — 通过率 {score}%", - "staleQuotaTooltip": "__MISSING__:Last refresh failed — showing cached data", - "quotaThresholdLabel": "__MISSING__:Min left", - "quotaCutoffsColumnHelp": "__MISSING__:Stop requests when remaining quota falls to this percentage or below.", - "quotaCutoffsButtonDefault": "__MISSING__:Default", - "quotaCutoffsButtonHelp": "__MISSING__:Edit minimum remaining quota cutoffs for this account.", - "quotaCutoffsButtonDisabled": "__MISSING__:No quota windows are available for this account yet.", - "quotaCutoffsTitle": "__MISSING__:Quota cutoffs for {name} ({provider})", - "quotaCutoffsExplainer": "__MISSING__:Override the minimum remaining quota percentage where this account stops being selected for each quota window. Leave blank to inherit the provider default.", - "quotaCutoffsDefaultHint": "__MISSING__:Default min remaining: {default}%", - "quotaCutoffsResetAll": "__MISSING__:Reset all", - "quotaCutoffsNoWindows": "__MISSING__:No quota windows are available for this account yet.", - "quotaThresholdInvalid": "__MISSING__:Enter a whole number from 0 to 100.", - "budgetKpiToday": "__MISSING__:Today", - "budgetKpiThisMonth": "__MISSING__:This month", - "budgetKpiProjEom": "__MISSING__:Proj EOM", - "budgetKpiBlocked": "__MISSING__:Blocked", - "budgetKpiAtRisk": "__MISSING__:At risk", - "budgetKpiActiveKeys": "__MISSING__:Active keys", - "budgetSearchKeysPlaceholder": "__MISSING__:Search keys...", - "budgetSortPctUsed": "__MISSING__:Sort: % Used ↓", - "budgetSortTodayDollar": "__MISSING__:Sort: Today $ ↓", - "budgetSortMonthDollar": "__MISSING__:Sort: Month $ ↓", - "budgetSortNameAZ": "__MISSING__:Sort: Name (A–Z)", - "budgetColDailyLim": "__MISSING__:Daily lim", - "budgetColMonthlyLim": "__MISSING__:Monthly lim", - "budgetColUsedPct": "__MISSING__:Used %", - "budgetLoading": "__MISSING__:Loading…", - "budgetNoKeysMatch": "__MISSING__:No keys match filters", - "budgetLinearExtrapolation": "__MISSING__:linear extrapolation", - "budgetThisMonthSoFar": "__MISSING__:This month so far", - "budgetProjectedEndOfMonth": "__MISSING__:Projected end of month", - "budgetByProvider": "__MISSING__:by provider", - "budgetDailyDollar": "__MISSING__:Daily $", - "budgetWeeklyDollar": "__MISSING__:Weekly $", - "budgetMonthlyDollar": "__MISSING__:Monthly $", - "budgetWarnAtPct": "__MISSING__:Warn at %", - "quotaAlerts": "__MISSING__:Quota alerts", - "quotaTableRefreshing": "__MISSING__:⟳ Refreshing...", - "noSpendLast30Days": "__MISSING__:No spend in last 30 days" + "staleQuotaTooltip": "上次刷新失败——正在显示缓存数据", + "quotaThresholdLabel": "最少剩余", + "quotaCutoffsColumnHelp": "当剩余额度降至此百分比或以下时停止请求。", + "quotaCutoffsButtonDefault": "默认", + "quotaCutoffsButtonHelp": "编辑此账号的最小剩余额度阈值。", + "quotaCutoffsButtonDisabled": "此账号尚无可用的额度窗口。", + "quotaCutoffsTitle": "{name}({provider})的额度阈值", + "quotaCutoffsExplainer": "覆盖每个额度窗口下该账号停止被选中的最小剩余额度百分比。留空则继承提供商默认值。", + "quotaCutoffsDefaultHint": "默认最小剩余:{default}%", + "quotaCutoffsResetAll": "全部重置", + "quotaCutoffsNoWindows": "此账号尚无可用的额度窗口。", + "quotaThresholdInvalid": "输入 0 到 100 之间的整数。", + "budgetKpiToday": "今天", + "budgetKpiThisMonth": "本月", + "budgetKpiProjEom": "预计月末", + "budgetKpiBlocked": "已阻止", + "budgetKpiAtRisk": "有风险", + "budgetKpiActiveKeys": "活跃密钥", + "budgetSearchKeysPlaceholder": "搜索密钥...", + "budgetSortPctUsed": "排序:已用 % ↓", + "budgetSortTodayDollar": "排序:今日 $ ↓", + "budgetSortMonthDollar": "排序:本月 $ ↓", + "budgetSortNameAZ": "排序:名称(A–Z)", + "budgetColDailyLim": "日限额", + "budgetColMonthlyLim": "月限额", + "budgetColUsedPct": "已用 %", + "budgetLoading": "正在加载…", + "budgetNoKeysMatch": "没有密钥匹配筛选条件", + "budgetLinearExtrapolation": "线性外推", + "budgetThisMonthSoFar": "本月至今", + "budgetProjectedEndOfMonth": "预计月末", + "budgetByProvider": "按提供商", + "budgetDailyDollar": "每日 $", + "budgetWeeklyDollar": "每周 $", + "budgetMonthlyDollar": "每月 $", + "budgetWarnAtPct": "警告阈值 %", + "quotaAlerts": "额度提醒", + "quotaTableRefreshing": "⟳ 正在刷新...", + "noSpendLast30Days": "过去 30 天无消费" }, "modals": { "waitingAuth": "等待授权", @@ -5459,18 +5459,18 @@ "quickStartStep4Title": "4. 设置客户端基本 URL", "quickStartStep4Prefix": "将您的 IDE 或 API 客户端指向", "quickStartStep4Suffix": "例如使用提供商前缀", - "deploySetupTitle": "__MISSING__:Setup Guide", - "deploySetupText": "__MISSING__:Step-by-step installation, environment configuration, and first-run walkthrough for OmniRoute.", - "deployElectronTitle": "__MISSING__:Electron Desktop", - "deployElectronText": "__MISSING__:Run OmniRoute as a native desktop application on Windows, macOS, and Linux.", - "deployDockerTitle": "__MISSING__:Docker", - "deployDockerText": "__MISSING__:Containerized deployment with Docker Compose; ready for production stacks and Kubernetes.", - "deployVmTitle": "__MISSING__:Virtual Machine", - "deployVmText": "__MISSING__:Self-host on any Linux VM. Includes systemd unit, log rotation, and backup tooling.", - "deployFlyTitle": "__MISSING__:Fly.io", - "deployFlyText": "__MISSING__:Deploy to Fly.io edge runtime with one fly.toml and a single deploy command.", - "deployPwaTitle": "__MISSING__:PWA", - "deployPwaText": "__MISSING__:Install OmniRoute as a Progressive Web App on Android, iOS, and desktop browsers.", + "deploySetupTitle": "安装指南", + "deploySetupText": "OmniRoute 的分步安装、环境配置和首次运行指南。", + "deployElectronTitle": "Electron 桌面版", + "deployElectronText": "在 Windows、macOS 和 Linux 上将 OmniRoute 作为原生桌面应用运行。", + "deployDockerTitle": "Docker", + "deployDockerText": "使用 Docker Compose 的容器化部署,适用于生产环境栈和 Kubernetes。", + "deployVmTitle": "虚拟机", + "deployVmText": "可在任意 Linux 虚拟机上自托管。包含 systemd 单元、日志轮转和备份工具。", + "deployFlyTitle": "Fly.io", + "deployFlyText": "通过一个 fly.toml 和一条部署命令即可部署到 Fly.io 边缘运行时。", + "deployPwaTitle": "PWA", + "deployPwaText": "可在 Android、iOS 和桌面浏览器上将 OmniRoute 安装为渐进式 Web 应用。", "deployTermuxTitle": "Termux (Android)", "deployTermuxText": "通过 Termux 在 Android 上以无界面模式运行 OmniRoute,并从移动浏览器访问仪表盘。", "featureRoutingTitle": "多提供商路由", @@ -5604,10 +5604,10 @@ "mcpToolsOperationsDesc": "路由模拟、预算保护、策略切换、韧性配置和提供商指标。", "mcpToolsCacheTitle": "缓存管理", "mcpToolsCacheDesc": "查看缓存统计,并清空语义缓存或签名缓存。", - "mcpToolsCompressionTitle": "__MISSING__:Compression Engines", - "mcpToolsCompressionDesc": "__MISSING__:Configure RTK/Brotli compression, swap engines, and inspect compression analytics by combo.", - "mcpToolsOneProxyTitle": "__MISSING__:1Proxy / Tunnels", - "mcpToolsOneProxyDesc": "__MISSING__:Manage outbound proxies, rotate residential IPs, and inspect proxy health.", + "mcpToolsCompressionTitle": "压缩引擎", + "mcpToolsCompressionDesc": "配置 RTK/Brotli 压缩、切换引擎,并按组合查看压缩分析。", + "mcpToolsOneProxyTitle": "1Proxy / 隧道", + "mcpToolsOneProxyDesc": "管理出站代理、轮换住宅 IP,并查看代理健康状态。", "mcpToolsMemoryTitle": "记忆", "mcpToolsMemoryDesc": "搜索、添加和清除持久化对话记忆条目。", "mcpToolsSkillsTitle": "技能", @@ -5751,55 +5751,55 @@ "fingerprintSettingsHint": "CLI 指纹匹配(伪装成特定 CLI 工具的请求)可在以下位置配置:", "settingsRoutingLink": "设置/路由", "openSettings": "设置", - "copyRawUrlTitle": "__MISSING__:Copy raw URL to clipboard", - "copied": "__MISSING__:Copied!", - "copyUrl": "__MISSING__:Copy URL", - "startHere": "__MISSING__:Start Here", - "badgeNew": "__MISSING__:New", - "viewOnGithub": "__MISSING__:View on GitHub", - "howToUse": "__MISSING__:How to use", - "browseAllSkillsOnGithub": "__MISSING__:Browse all skills on GitHub", - "apiSkills": "__MISSING__:API Skills", - "cliSkills": "__MISSING__:CLI Skills" + "copyRawUrlTitle": "复制原始 URL 到剪贴板", + "copied": "已复制!", + "copyUrl": "复制 URL", + "startHere": "从这里开始", + "badgeNew": "新增", + "viewOnGithub": "在 GitHub 上查看", + "howToUse": "使用方法", + "browseAllSkillsOnGithub": "在 GitHub 上浏览所有技能", + "apiSkills": "API 技能", + "cliSkills": "CLI 技能" }, "cloudAgents": { - "title": "__MISSING__:Cloud Agents", - "description": "__MISSING__:Manage autonomous coding agents (Jules, Devin, Codex Cloud)", - "loading": "__MISSING__:Loading tasks...", - "aboutTitle": "__MISSING__:About Cloud Agents", - "aboutDescription": "__MISSING__:Cloud agents are remote AI coding assistants that can execute tasks autonomously. They work differently from local CLI agents - you interact with them through OmniRoute's API.", - "howItWorksTitle": "__MISSING__:How it works:", - "howItWorksDesc": "__MISSING__:Create a task → Agent analyzes and proposes a plan → You approve → Agent executes → Results returned", - "newTaskTitle": "__MISSING__:Create New Task", - "newTaskDescription": "__MISSING__:Start a new task with a cloud agent", - "selectAgent": "__MISSING__:Select Agent", - "taskDescription": "__MISSING__:Task Description", - "taskDescriptionPlaceholder": "__MISSING__:Describe what you want the agent to do...", - "startTask": "__MISSING__:Start Task", - "tasks": "__MISSING__:Tasks", - "taskDetail": "__MISSING__:Task Detail", - "noTasks": "__MISSING__:No tasks yet. Create one to get started.", - "untitledTask": "__MISSING__:Untitled Task", - "created": "__MISSING__:Created", - "conversation": "__MISSING__:Conversation", - "result": "__MISSING__:Result", - "error": "__MISSING__:Error", - "planReady": "__MISSING__:Plan Ready for Approval", - "approvePlan": "__MISSING__:Approve Plan", - "rejectPlan": "__MISSING__:Reject & Cancel", - "sendMessagePlaceholder": "__MISSING__:Send a message to the agent...", - "cancel": "__MISSING__:Cancel", - "delete": "__MISSING__:Delete", - "selectTaskPrompt": "__MISSING__:Select a task to view details", - "statusPending": "__MISSING__:Pending", - "statusRunning": "__MISSING__:Running", - "statusWaitingApproval": "__MISSING__:Waiting Approval", - "statusCompleted": "__MISSING__:Completed", - "statusFailed": "__MISSING__:Failed", - "statusCancelled": "__MISSING__:Cancelled", - "repositoryName": "__MISSING__:Repository name", - "repositoryUrl": "__MISSING__:Repository URL", - "branch": "__MISSING__:Branch" + "title": "云端 Agents", + "description": "管理自主编码 Agent(Jules、Devin、Codex Cloud)", + "loading": "正在加载任务...", + "aboutTitle": "关于云端 Agents", + "aboutDescription": "云端 Agent 是远程 AI 编码助手,可以自主执行任务。它们与本地 CLI Agent 的工作方式不同——你通过 OmniRoute 的 API 与它们交互。", + "howItWorksTitle": "工作方式:", + "howItWorksDesc": "创建任务 → Agent 分析并提出方案 → 你批准 → Agent 执行 → 返回结果", + "newTaskTitle": "创建新任务", + "newTaskDescription": "使用云端 Agent 开始一个新任务", + "selectAgent": "选择 Agent", + "taskDescription": "任务描述", + "taskDescriptionPlaceholder": "描述你希望 Agent 执行的内容...", + "startTask": "开始任务", + "tasks": "任务", + "taskDetail": "任务详情", + "noTasks": "暂无任务。创建一个即可开始。", + "untitledTask": "未命名任务", + "created": "已创建", + "conversation": "对话", + "result": "结果", + "error": "错误", + "planReady": "方案已准备好等待批准", + "approvePlan": "批准方案", + "rejectPlan": "拒绝并取消", + "sendMessagePlaceholder": "向 Agent 发送消息...", + "cancel": "取消", + "delete": "删除", + "selectTaskPrompt": "选择要查看详情的任务", + "statusPending": "待处理", + "statusRunning": "运行中", + "statusWaitingApproval": "等待批准", + "statusCompleted": "已完成", + "statusFailed": "失败", + "statusCancelled": "已取消", + "repositoryName": "仓库名称", + "repositoryUrl": "仓库 URL", + "branch": "分支" }, "templateNames": { "simple-chat": "简单对话", @@ -5808,8 +5808,8 @@ "thinking": "思考模式", "tool-calling": "工具调用", "multi-turn": "多轮对话", - "vision": "__MISSING__:Vision", - "schema-coercion": "__MISSING__:Schema Coercion" + "vision": "视觉", + "schema-coercion": "Schema 强制转换" }, "templateDescriptions": { "simple-chat": "带系统消息的基础对话模板", @@ -5818,8 +5818,8 @@ "thinking": "带推理/思考预算的模板", "tool-calling": "用于工具/函数调用的模板", "multi-turn": "用于多轮对话的模板", - "vision": "__MISSING__:Multimodal template with image input", - "schema-coercion": "__MISSING__:Structured-output / JSON schema enforcement" + "vision": "带图像输入的多模态模板", + "schema-coercion": "结构化输出 / JSON schema 强制约束" }, "templatePayloads": { "simpleChat": { @@ -5966,12 +5966,12 @@ "reasoningClearSuccess": "已清除 {count} 个推理缓存条目", "reasoningClearError": "清除推理缓存失败", "reasoningNoData": "尚未缓存推理条目。思考模型使用工具调用时会显示条目。", - "cachePerformanceRetry": "__MISSING__:Retry", - "cachePerformanceHitRate": "__MISSING__:Hit Rate", - "cachePerformanceAvgLatency": "__MISSING__:Avg Latency (ms)", - "cachePerformanceP95Latency": "__MISSING__:p95 Latency (ms)", - "retry": "__MISSING__:Retry", - "reasoningAvgChars": "__MISSING__:Avg Chars" + "cachePerformanceRetry": "重试", + "cachePerformanceHitRate": "命中率", + "cachePerformanceAvgLatency": "平均延迟(ms)", + "cachePerformanceP95Latency": "p95 延迟(ms)", + "retry": "重试", + "reasoningAvgChars": "平均字符数" }, "proxyConfigModal": { "levelGlobal": "全局", @@ -6152,8 +6152,8 @@ "bulkImportErrorInvalidPort": "PORT 无效(必须为 1-65535)", "bulkImportErrorInvalidType": "TYPE 无效(使用 http、https 或 socks5)", "bulkImportErrorInvalidStatus": "STATUS 无效(使用 active 或 inactive)", - "clearAssignment": "__MISSING__:(clear assignment)", - "bulkProxyAssignment": "__MISSING__:Bulk Proxy Assignment" + "clearAssignment": "(清除分配)", + "bulkProxyAssignment": "批量代理分配" }, "playground": { "title": "模型演练场", @@ -6195,9 +6195,9 @@ "rerank": "Rerank", "search": "网页搜索" }, - "conversationalChat": "__MISSING__:Conversational Chat", - "clearChat": "__MISSING__:Clear chat", - "typeMessagePlaceholder": "__MISSING__:Type a message... (Shift+Enter for new line)" + "conversationalChat": "对话式聊天", + "clearChat": "清空聊天", + "typeMessagePlaceholder": "输入消息...(Shift+Enter 换行)" }, "requestLogger": { "recording": "正在录制", @@ -6436,9 +6436,9 @@ "addKey": "+ Add key…", "equalSplit": "Equal split", "save": "Save allocations", - "betaPreviewLabel": "__MISSING__:Beta — UI preview.", - "betaConfigSavedPrefix": "__MISSING__:A configuração é salva em", - "betaConfigSavedSuffix": "__MISSING__:(não persiste no servidor ainda). A aplicação dos caps por request ainda não está conectada ao pipeline da proxy. Esta tela permite desenhar e visualizar a divisão de cota;", - "policyLabel": "__MISSING__:Policy:" + "betaPreviewLabel": "Beta — UI 预览。", + "betaConfigSavedPrefix": "配置已保存于", + "betaConfigSavedSuffix": "(尚未持久化到服务器)。按请求的配额上限应用仍未接入代理管道。此页面可用于设计并预览配额分配;", + "policyLabel": "策略:" } } From 1f2d1fdbf2a630895c6af260ac60e2619e92c1eb Mon Sep 17 00:00:00 2001 From: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Date: Fri, 22 May 2026 00:11:58 +0300 Subject: [PATCH 042/112] fix(i18n): add missing dashboard keys and fix EN fallbacks (#2500) Integrated into release/v3.8.2 --- src/app/(dashboard)/dashboard/cache/page.tsx | 2 +- .../caveman/CavemanContextPageClient.tsx | 6 +-- .../dashboard/costs/CostOverviewTab.tsx | 8 ++- .../providers/components/ProviderCard.tsx | 4 +- .../usage/components/ProviderLimits/index.tsx | 18 +++---- src/i18n/messages/ar.json | 48 ++++++++++++++++- src/i18n/messages/az.json | 48 ++++++++++++++++- src/i18n/messages/bg.json | 48 ++++++++++++++++- src/i18n/messages/bn.json | 48 ++++++++++++++++- src/i18n/messages/cs.json | 48 ++++++++++++++++- src/i18n/messages/da.json | 48 ++++++++++++++++- src/i18n/messages/de.json | 48 ++++++++++++++++- src/i18n/messages/en.json | 48 ++++++++++++++++- src/i18n/messages/es.json | 48 ++++++++++++++++- src/i18n/messages/fa.json | 48 ++++++++++++++++- src/i18n/messages/fi.json | 48 ++++++++++++++++- src/i18n/messages/fr.json | 48 ++++++++++++++++- src/i18n/messages/gu.json | 48 ++++++++++++++++- src/i18n/messages/he.json | 48 ++++++++++++++++- src/i18n/messages/hi.json | 48 ++++++++++++++++- src/i18n/messages/hu.json | 48 ++++++++++++++++- src/i18n/messages/id.json | 48 ++++++++++++++++- src/i18n/messages/in.json | 48 ++++++++++++++++- src/i18n/messages/it.json | 48 ++++++++++++++++- src/i18n/messages/ja.json | 48 ++++++++++++++++- src/i18n/messages/ko.json | 48 ++++++++++++++++- src/i18n/messages/mr.json | 48 ++++++++++++++++- src/i18n/messages/ms.json | 48 ++++++++++++++++- src/i18n/messages/nl.json | 48 ++++++++++++++++- src/i18n/messages/no.json | 48 ++++++++++++++++- src/i18n/messages/phi.json | 48 ++++++++++++++++- src/i18n/messages/pl.json | 48 ++++++++++++++++- src/i18n/messages/pt-BR.json | 48 ++++++++++++++++- src/i18n/messages/pt.json | 48 ++++++++++++++++- src/i18n/messages/ro.json | 48 ++++++++++++++++- src/i18n/messages/ru.json | 48 ++++++++++++++++- src/i18n/messages/sk.json | 48 ++++++++++++++++- src/i18n/messages/sv.json | 48 ++++++++++++++++- src/i18n/messages/sw.json | 48 ++++++++++++++++- src/i18n/messages/ta.json | 48 ++++++++++++++++- src/i18n/messages/te.json | 48 ++++++++++++++++- src/i18n/messages/th.json | 48 ++++++++++++++++- src/i18n/messages/tr.json | 48 ++++++++++++++++- src/i18n/messages/uk-UA.json | 48 ++++++++++++++++- src/i18n/messages/ur.json | 48 ++++++++++++++++- src/i18n/messages/vi.json | 48 ++++++++++++++++- tests/unit/provider-limits-ui.test.ts | 52 +++++++++++++++++++ 47 files changed, 1999 insertions(+), 59 deletions(-) diff --git a/src/app/(dashboard)/dashboard/cache/page.tsx b/src/app/(dashboard)/dashboard/cache/page.tsx index 42080fb85d..964f56e2c9 100644 --- a/src/app/(dashboard)/dashboard/cache/page.tsx +++ b/src/app/(dashboard)/dashboard/cache/page.tsx @@ -481,7 +481,7 @@ export default function CachePage() {
{loading && ( -
+
)} diff --git a/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx b/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx index a68fa1499b..31fde6ba1a 100644 --- a/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx +++ b/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx @@ -233,10 +233,8 @@ export default function CavemanContextPageClient() { )}
-

Input compression

-

- Rewrite chat history with shorter wording. Reduces input tokens by ~50%. -

+

{t("inputCompressionTitle")}

+

{t("inputCompressionDesc")}

@@ -1095,7 +1097,7 @@ function TopListCard({ {hasCostData || Number(row[valueKey] || 0) > 0 ? ( currencyFormatter.format(Number(row[valueKey] || 0)) ) : ( - Legacy / Free + {t("legacyFreeLabel")} )}
@@ -1118,11 +1120,13 @@ function CostBreakdownTable({ rows, columns, locale, + legacyFreeLabel, }: { title: string; rows: Array>; columns: ColumnDef[]; locale: string; + legacyFreeLabel: string; }) { const currencyFormatter = createCurrencyFormatter(locale); @@ -1130,7 +1134,7 @@ function CostBreakdownTable({ const num = Number(value || 0); switch (format) { case "currency": - return num > 0 ? currencyFormatter.format(num) : "Legacy / Free"; + return num > 0 ? currencyFormatter.format(num) : legacyFreeLabel; case "compact": return new Intl.NumberFormat(locale, { notation: "compact" }).format(num); case "number": diff --git a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx index c1bb12c693..969fa4d049 100644 --- a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx @@ -115,10 +115,10 @@ export default function ProviderCard({ bolt - Fast + {t("tierFast")} ) : null; diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index 710d2e2f61..27642823f1 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -70,7 +70,7 @@ const TIER_FILTERS = [ { key: "ultra", labelKey: "tierUltra" }, { key: "pro", labelKey: "tierPro" }, { key: "plus", labelKey: "tierPlus" }, - { key: "lite", label: "Lite" }, + { key: "lite", labelKey: "tierLite" }, { key: "free", labelKey: "tierFree" }, { key: "unknown", labelKey: "tierUnknown" }, ]; @@ -804,9 +804,9 @@ export default function ProviderLimits() { const tone = STATUS_TONE[key]; const labelMap: Record = { all: tr("statTotal", "Total"), - critical: tr("statCritical", "Crítico"), - alert: tr("statAlert", "Alerta"), - ok: tr("statHealthy", "Saudável"), + critical: tr("statCritical", "Critical"), + alert: tr("statAlert", "Alert"), + ok: tr("statHealthy", "Healthy"), }; const active = statusFilter === key; const count = statusCounts[key] || 0; @@ -847,7 +847,7 @@ export default function ProviderLimits() { {/* Purchase Type Filter */}
- {tr("filterPurchaseTypeLabel", "Tipo")} + {tr("filterPurchaseTypeLabel", "Type")} {PURCHASE_TYPES.map((type) => { const count = purchaseTypeCounts[type.key] || 0; @@ -985,7 +985,7 @@ export default function ProviderLimits() { {formatQuotaLabel(q.name) || tr("creditsLabel", "Credits")}
- {tr("creditBalanceHint", "Saldo restante")} + {tr("creditBalanceHint", "Remaining balance")}
@@ -1039,7 +1039,7 @@ export default function ProviderLimits() { ) : cd ? ( - ⏱ {tr("resetsIn", "reset em")} {cd} + ⏱ {tr("resetsIn", "Resets in")} {cd} ) : null} tune - {tr("editCutoffs", "Editar Cutoffs")} + {tr("editCutoffs", "Edit cutoffs")} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index ce2ef3d7c1..fba329e687 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "نقطة نهاية واجهة برمجة التطبيقات", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "برو", "tierPlus": "زائد", "tierFree": "مجاني", + "tierLite": "__MISSING__:Lite", "tierUnknown": "غير معروف", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 8ddc8d67d9..1cc0630f45 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "Previous Half", "currentPeriod": "Current Half", "exportCSV": "Export as CSV", - "exportJSON": "Export as JSON" + "exportJSON": "Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "my-gcp-project-id", "antigravityProjectIdHint": "Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "Google Cloud Project ID", "antigravityProjectIdPlaceholder": "my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "Clone", "exportSuite": "Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index b232e36310..2070734f80 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Крайна точка на API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Професионалист", "tierPlus": "плюс", "tierFree": "безплатно", + "tierLite": "__MISSING__:Lite", "tierUnknown": "неизвестен", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 27782d5a1a..c6615e340e 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 9f32a8a7ed..cd635585da 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Doporučení aplikováno na toto kombo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Koncový bod", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Zdarma", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Neznámý", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Vymazání mezipaměti selhalo.", "unavailable": "Mezipaměť nedostupná", "unavailableDesc": "Nepodařilo se načíst statistiky mezipaměti. Ujistěte se, že server běží.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt mezipaměť (na straně poskytovatele)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 661666ecd6..d3a8164cf5 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API-endepunkt", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Gratis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Ukendt", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2db0c2f174..71bf4ed736 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Empfehlungen für dieses Combo wurden angewendet.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API-Endpunkt", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "service_tier=priority global für OpenAI Codex-Anfragen einfügen.", "codexFastTierHint": "Wenn aktiviert, fügt OmniRoute ausgehenden Codex-Anfragen service_tier=priority hinzu, sofern für die Verbindung noch kein Tier festgelegt ist. Der Priority-Tier erfordert einen OpenAI Enterprise API-Schlüssel oder den ChatGPT-Auth-Codex-Pfad; andere Schlüsseltypen erhalten von OpenAI einen tier-bezogenen Fehler. Pro-Verbindung-Einstellungen auf der Codex-Provider-Seite haben Vorrang.", "codexFastTierSaveError": "Codex Fast Tier-Einstellung konnte nicht aktualisiert werden", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "Claude Fast Mode", "claudeFastModeDesc": "Aktiviert den Fast Mode (speed:\"fast\") für ausgewählte Claude-Anfragen.", "claudeFastModeHint": "Anthropic unterstützt Fast Mode für SDK-Clients offiziell nicht. Wenn aktiviert, fügt OmniRoute den Header X-CPA-Force-Fast-Mode hinzu, sodass ein kompatibler CLIProxyAPI-Build den Entrypoint umschreiben kann. Nur die aufgeführten Opus-Modelle passieren die clientseitige Anthropic-Prüfung. Abonnement, Max-Plan und Fast-Mode-Credit-Saldo werden weiterhin serverseitig durchgesetzt — Anthropic kann auch bei aktiviertem Toggle out_of_credits zurückgeben.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Profi", "tierPlus": "Plus", "tierFree": "Kostenlos", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unbekannt", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 83217c62cf..e355954350 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "Configuration View", + "configOnlyHint": "This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "Routing Inputs", + "routingInputsHint": "Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "Manual model", + "manualModelInvalid": "Enter a model as provider/model.", + "manualModelUnknownProvider": "Unknown provider prefix.", + "builderDynamicAccountShort": "Dynamic account", + "builderNeedValidName": "Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "Previous Half", "currentPeriod": "Current Half", "exportCSV": "Export as CSV", - "exportJSON": "Export as JSON" + "exportJSON": "Export as JSON", + "legacyFreeLabel": "Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "my-gcp-project-id", "antigravityProjectIdHint": "Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "Client profile", + "antigravityClientProfileHint": "Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "IDE", + "antigravityClientProfileHarness": "Harness / CLI", + "codexFastTierActiveChip": "Codex Fast tier is active", + "tierFast": "Fast", "antigravityProjectIdLabel": "Google Cloud Project ID", "antigravityProjectIdPlaceholder": "my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "Service tier", + "codexFastTierTierPriority": "Priority", + "codexFastTierTierFlex": "Flex", + "codexFastTierTierDefault": "Default", + "codexFastTierModelsLabel": "Fast-tier models", + "codexFastTierModelsHint": "Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "Enable Fast Tier for {model}", "claudeFastModeTitle": "Claude Fast Mode", "claudeFastModeDesc": "Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "Input compression", + "inputCompressionDesc": "Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "Lite", "tierUnknown": "Unknown", + "statTotal": "Total", + "statCritical": "Critical", + "statAlert": "Alert", + "statHealthy": "Healthy", + "filterPurchaseTypeLabel": "Type", + "filterTierLabel": "Tier", + "purchaseAll": "All", + "purchaseOauthSub": "Subscription", + "purchaseOauthFree": "OAuth Free", + "purchaseApiKey": "API Key", + "creditsLabel": "Credits", + "creditBalanceHint": "Remaining balance", + "unlimitedLabel": "Unlimited", + "refreshing": "Refreshing", + "resetsIn": "Resets in", + "editCutoffs": "Edit cutoffs", + "forceRefresh": "Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "Clone", "exportSuite": "Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index b7097c3e00..33afc3b3e9 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Se aplicaron recomendaciones a este combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Punto final API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "Inyectar globalmente service_tier=priority para las solicitudes de OpenAI Codex.", "codexFastTierHint": "Cuando está habilitado, OmniRoute añade service_tier=priority a las solicitudes salientes de Codex para las conexiones que aún no especifican un tier. El tier priority requiere una clave API de OpenAI Enterprise o la ruta de Codex con autenticación ChatGPT; otros tipos de claves recibirán un error relacionado con el tier de OpenAI. Los ajustes por conexión en la página del proveedor Codex tienen prioridad.", "codexFastTierSaveError": "Error al actualizar el ajuste de Codex Fast Tier", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "Claude Fast Mode", "claudeFastModeDesc": "Activar el Fast Mode de Anthropic (speed:\"fast\") en las solicitudes Claude seleccionadas.", "claudeFastModeHint": "Anthropic no admite oficialmente Fast Mode para clientes en modo SDK. Cuando está habilitado, OmniRoute reenvía un encabezado X-CPA-Force-Fast-Mode para que una compilación compatible de CLIProxyAPI pueda reescribir el entrypoint. Solo los modelos Opus listados pasan la verificación del binario de Anthropic. La suscripción, el plan Max y el saldo de créditos Fast Mode siguen aplicándose en el servidor — Anthropic puede devolver out_of_credits incluso con el toggle activado.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "profesional", "tierPlus": "Más", "tierFree": "Gratis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Desconocido", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index ac33a71919..73cace1a89 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 8e81b81e35..c6241b51d0 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API-päätepiste", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Ilmainen", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Tuntematon", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 42f00282e6..2ac1126448 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommandations appliquées à ce combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Point de terminaison de l'API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "Injecter globalement service_tier=priority pour les requêtes OpenAI Codex.", "codexFastTierHint": "Lorsqu'activé, OmniRoute ajoute service_tier=priority aux requêtes Codex sortantes pour les connexions qui n'ont pas déjà défini un tier. Le tier priority nécessite une clé API OpenAI Enterprise ou le chemin Codex avec authentification ChatGPT ; les autres types de clés recevront une erreur liée au tier de la part d'OpenAI. Les paramètres par connexion sur la page du provider Codex prévalent.", "codexFastTierSaveError": "Échec de la mise à jour du paramètre Codex Fast Tier", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "Claude Fast Mode", "claudeFastModeDesc": "Activer le Fast Mode Anthropic (speed:\"fast\") pour les requêtes Claude sélectionnées.", "claudeFastModeHint": "Anthropic ne prend pas officiellement en charge le Fast Mode pour les clients de type SDK. Lorsque activé, OmniRoute transmet un en-tête X-CPA-Force-Fast-Mode permettant à un CLIProxyAPI compatible de réécrire l'entrypoint. Seuls les modèles Opus listés sont autorisés par la vérification côté binaire d'Anthropic. L'abonnement, le plan Max et le solde de crédits Fast Mode restent appliqués côté serveur — Anthropic peut renvoyer out_of_credits même lorsque le toggle est activé.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Gratuit", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Inconnu", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 6620c56b84..22e3fdd464 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index b6a6612ea6..b838cf4aa6 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "נקודת קצה API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "פרו", "tierPlus": "בנוסף", "tierFree": "חינם", + "tierLite": "__MISSING__:Lite", "tierUnknown": "לא ידוע", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index adff1b4e4f..ab562a1c56 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "एपीआई समापन बिंदु", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "प्रो", "tierPlus": "प्लस", "tierFree": "निःशुल्क", + "tierLite": "__MISSING__:Lite", "tierUnknown": "अज्ञात", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 7064c4dcc4..0d12832a3a 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API végpont", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plusz", "tierFree": "Ingyenes", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Ismeretlen", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index fa0b2932a2..aa2a17f8ec 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Titik Akhir API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Ditambah lagi", "tierFree": "Gratis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Tidak diketahui", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 09f764197b..5e44b9bf42 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 45a3a58107..86b7dc543a 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Endpoint API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Più", "tierFree": "Gratuito", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Sconosciuto", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 6d934dfc61..82fdedf03f 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "APIエンドポイント", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "プロ", "tierPlus": "プラス", "tierFree": "無料", + "tierLite": "__MISSING__:Lite", "tierUnknown": "不明", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index c730f4dcc0..6fdcaf1d96 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API 엔드포인트", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "프로", "tierPlus": "플러스", "tierFree": "무료", + "tierLite": "__MISSING__:Lite", "tierUnknown": "알 수 없음", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 23f3841ca7..401e914b6c 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 1ec8c3dac8..5294122df9 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Titik Akhir API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Tambahan pula", "tierFree": "Percuma", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Tidak diketahui", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 6e29c408bf..5ee7e54762 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API-eindpunt", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Gratis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Onbekend", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 1ebc175484..07fb6394ec 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API-endepunkt", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Pluss", "tierFree": "Gratis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Ukjent", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 671e3815b6..f1eaea54a8 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Endpoint ng API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Dagdag pa", "tierFree": "Libre", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Hindi alam", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 4c8e58c232..8396aef856 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Punkt końcowy interfejsu API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Zawodowiec", "tierPlus": "Dodatkowo", "tierFree": "Bezpłatny", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Nieznany", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index baaa004d24..d319086757 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Endpoint da API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Cole o cookie sso do grok.com. Um valor completo `sso=...` também funciona.", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Ativo", "autoDetect": "Auto-detectar idioma", "rulesCount": "{count} regras", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Analytics de Compressao", "noAnalytics": "Sem analytics de compressao ainda.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "Lite", "tierUnknown": "Desconhecido", + "statTotal": "Total", + "statCritical": "Crítico", + "statAlert": "Alerta", + "statHealthy": "Saudável", + "filterPurchaseTypeLabel": "Tipo", + "filterTierLabel": "Tier", + "purchaseAll": "Todos", + "purchaseOauthSub": "Assinatura", + "purchaseOauthFree": "OAuth gratuito", + "purchaseApiKey": "Chave de API", + "creditsLabel": "Créditos", + "creditBalanceHint": "Saldo restante", + "unlimitedLabel": "Ilimitado", + "refreshing": "Atualizando", + "resetsIn": "Reinicia em", + "editCutoffs": "Editar limites", + "forceRefresh": "Atualizar agora", "suiteBuilderSaveFailed": "Falha ao salvar a suíte customizada", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 43ae09c023..5b797d2da1 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Ponto final da API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pró", "tierPlus": "Mais", "tierFree": "Grátis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Desconhecido", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index f184bb57c0..39488dcfa9 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Gratuit", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Necunoscut", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index f88e1e3466..507addaef5 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Рекомендации применены к этому комбо.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "Только конфигурация", + "configOnlyHint": "Здесь только входные параметры маршрутизации. Состояние circuit breaker — на странице Health.", + "routingInputs": "Параметры маршрутизации", + "routingInputsHint": "Mode pack и веса здесь; runtime circuit breaker — на странице Health.", + "emailVisibilityHint": "Email аккаунтов следует глобальному переключателю приватности.", + "emailVisibilityTooltip": "Иконка глаза скрывает или показывает email во всех разделах: combos, providers и quota.", + "manualModel": "Модель вручную", + "manualModelInvalid": "Введите модель как provider/model.", + "manualModelUnknownProvider": "Неизвестный префикс провайдера.", + "builderDynamicAccountShort": "Динамический аккаунт", + "builderNeedValidName": "Укажите корректное имя combo перед продолжением.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "Legacy / Free" }, "endpoint": { "title": "Конечная точка API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "Профиль клиента", + "antigravityClientProfileHint": "Какой клиент Antigravity представляет OmniRoute при запросах к API.", + "antigravityClientProfileIde": "IDE", + "antigravityClientProfileHarness": "Harness / CLI", + "codexFastTierActiveChip": "Активен Codex Fast tier", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "Уровень сервиса", + "codexFastTierTierPriority": "Priority", + "codexFastTierTierFlex": "Flex", + "codexFastTierTierDefault": "Default", + "codexFastTierModelsLabel": "Модели Fast Tier", + "codexFastTierModelsHint": "service_tier применяется только к отмеченным моделям при включённом Fast Tier.", + "codexFastTierModelCheckbox": "Включить Fast Tier для {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Про", "tierPlus": "Плюс", "tierFree": "Бесплатно", + "tierLite": "Lite", "tierUnknown": "Неизвестно", + "statTotal": "Всего", + "statCritical": "Критично", + "statAlert": "Внимание", + "statHealthy": "Норма", + "filterPurchaseTypeLabel": "Тип", + "filterTierLabel": "Тариф", + "purchaseAll": "Все", + "purchaseOauthSub": "Подписка", + "purchaseOauthFree": "OAuth бесплатно", + "purchaseApiKey": "API-ключ", + "creditsLabel": "Кредиты", + "creditBalanceHint": "Остаток баланса", + "unlimitedLabel": "Без лимита", + "refreshing": "Обновление", + "resetsIn": "Сброс через", + "editCutoffs": "Изменить пороги", + "forceRefresh": "Обновить сейчас", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Не удалось очистить кэш.", "unavailable": "Кэш недоступен", "unavailableDesc": "Не удалось получить статистику кэша. Убедитесь, что сервер запущен.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Кэш промптов (на стороне провайдера)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index a58e17307b..2f0b18b111 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Koncový bod API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Navyše", "tierFree": "Zadarmo", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Neznámy", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index cd4a46bcd0..b8f28cedee 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API-slutpunkt", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Gratis", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Okänd", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 09f764197b..5e44b9bf42 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 1f46c9e124..378a38879e 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 5a6c9fabf8..c892f4baf7 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 0ef05b2221..e68166164c 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "จุดสิ้นสุด API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "โปร", "tierPlus": "บวก", "tierFree": "ฟรี", + "tierLite": "__MISSING__:Lite", "tierUnknown": "ไม่ทราบ", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 630aa723a2..18eea53e42 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Öneriler bu komboya uygulandı.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Uç Noktası", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Profesyonel", "tierPlus": "Artı", "tierFree": "Ücretsiz", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Bilinmiyor", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 52cf95bc37..4e675d7d8b 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Кінцева точка API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Плюс", "tierFree": "безкоштовно", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Невідомий", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 219223610e..94ab8f3f72 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "API Endpoint", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "Pro", "tierPlus": "Plus", "tierFree": "Free", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Unknown", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 95406cc079..2371943ba1 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -2030,6 +2030,17 @@ "recommendationsApplied": "Recommendations applied to this combo.", "intelligentPanelTitle": "Intelligent Routing Dashboard", "intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.", + "configOnlyStatus": "__MISSING__:Configuration View", + "configOnlyHint": "__MISSING__:This panel shows routing inputs only. Live breaker state is available on the Health page.", + "routingInputs": "__MISSING__:Routing Inputs", + "routingInputsHint": "__MISSING__:Mode pack and weighting stay here; breaker runtime state stays on the Health page.", + "emailVisibilityHint": "__MISSING__:Account emails here follow the global privacy toggle.", + "emailVisibilityTooltip": "__MISSING__:Use the eye icon to reveal or hide account emails globally across combos, providers and quota screens.", + "manualModel": "__MISSING__:Manual model", + "manualModelInvalid": "__MISSING__:Enter a model as provider/model.", + "manualModelUnknownProvider": "__MISSING__:Unknown provider prefix.", + "builderDynamicAccountShort": "__MISSING__:Dynamic account", + "builderNeedValidName": "__MISSING__:Define a valid combo name before continuing.", "statusOverview": "Status Overview", "normalOperation": "Normal Operation", "allProvidersHealthy": "Providers are reporting healthy routing conditions.", @@ -2339,7 +2350,8 @@ "previousPeriod": "__MISSING__:Previous Half", "currentPeriod": "__MISSING__:Current Half", "exportCSV": "__MISSING__:Export as CSV", - "exportJSON": "__MISSING__:Export as JSON" + "exportJSON": "__MISSING__:Export as JSON", + "legacyFreeLabel": "__MISSING__:Legacy / Free" }, "endpoint": { "title": "Điểm cuối API", @@ -3622,6 +3634,12 @@ "geminiCliProjectIdLabel": "__MISSING__:Google Cloud Project ID", "geminiCliProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "antigravityProjectIdHint": "__MISSING__:Optional override for Antigravity Cloud Code requests. Leave blank to use the project discovered during Google OAuth.", + "antigravityClientProfileLabel": "__MISSING__:Client profile", + "antigravityClientProfileHint": "__MISSING__:Choose which Antigravity client identity OmniRoute presents to the API.", + "antigravityClientProfileIde": "__MISSING__:IDE", + "antigravityClientProfileHarness": "__MISSING__:Harness / CLI", + "codexFastTierActiveChip": "__MISSING__:Codex Fast tier is active", + "tierFast": "__MISSING__:Fast", "antigravityProjectIdLabel": "__MISSING__:Google Cloud Project ID", "antigravityProjectIdPlaceholder": "__MISSING__:my-gcp-project-id", "grokWebCookieHint": "Grok Web Cookie Hint", @@ -4557,6 +4575,13 @@ "codexFastTierDesc": "__MISSING__:Globally inject service_tier=priority for OpenAI Codex requests.", "codexFastTierHint": "__MISSING__:When enabled, OmniRoute adds service_tier=priority to outbound Codex requests for connections that don't already specify a tier. Priority tier requires an OpenAI Enterprise API key or the ChatGPT-auth Codex path; other key types will receive a tier-related error from OpenAI. Per-connection settings on the Codex provider page take precedence.", "codexFastTierSaveError": "__MISSING__:Failed to update Codex Fast Tier setting", + "codexFastTierTierLabel": "__MISSING__:Service tier", + "codexFastTierTierPriority": "__MISSING__:Priority", + "codexFastTierTierFlex": "__MISSING__:Flex", + "codexFastTierTierDefault": "__MISSING__:Default", + "codexFastTierModelsLabel": "__MISSING__:Fast-tier models", + "codexFastTierModelsHint": "__MISSING__:Only checked models receive service_tier when Fast Tier is enabled.", + "codexFastTierModelCheckbox": "__MISSING__:Enable Fast Tier for {model}", "claudeFastModeTitle": "__MISSING__:Claude Fast Mode", "claudeFastModeDesc": "__MISSING__:Opt selected Claude requests into Anthropic Fast Mode (speed:\"fast\").", "claudeFastModeHint": "__MISSING__:Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", @@ -4657,6 +4682,8 @@ "enabled": "Enabled", "autoDetect": "Auto-detect language", "rulesCount": "{count} rules", + "inputCompressionTitle": "__MISSING__:Input compression", + "inputCompressionDesc": "__MISSING__:Rewrite chat history with shorter wording. Reduces input tokens by ~50%.", "analyticsTitle": "Compression Analytics", "noAnalytics": "No compression analytics yet.", "outputMode": "__MISSING__:Output Mode", @@ -5058,7 +5085,25 @@ "tierPro": "chuyên nghiệp", "tierPlus": "Cộng thêm", "tierFree": "miễn phí", + "tierLite": "__MISSING__:Lite", "tierUnknown": "Không xác định", + "statTotal": "__MISSING__:Total", + "statCritical": "__MISSING__:Critical", + "statAlert": "__MISSING__:Alert", + "statHealthy": "__MISSING__:Healthy", + "filterPurchaseTypeLabel": "__MISSING__:Type", + "filterTierLabel": "__MISSING__:Tier", + "purchaseAll": "__MISSING__:All", + "purchaseOauthSub": "__MISSING__:Subscription", + "purchaseOauthFree": "__MISSING__:OAuth Free", + "purchaseApiKey": "__MISSING__:API Key", + "creditsLabel": "__MISSING__:Credits", + "creditBalanceHint": "__MISSING__:Remaining balance", + "unlimitedLabel": "__MISSING__:Unlimited", + "refreshing": "__MISSING__:Refreshing", + "resetsIn": "__MISSING__:Resets in", + "editCutoffs": "__MISSING__:Edit cutoffs", + "forceRefresh": "__MISSING__:Refresh now", "suiteBuilderSaveFailed": "Failed to save suite", "clone": "__MISSING__:Clone", "exportSuite": "__MISSING__:Export", @@ -5879,6 +5924,7 @@ "clearError": "Failed to clear cache.", "unavailable": "Cache unavailable", "unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.", + "loadingCacheAria": "__MISSING__:Loading cache", "promptCache": "Prompt Cache (Provider-Side)", "semanticCache": "Semantic Cache", "promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.", diff --git a/tests/unit/provider-limits-ui.test.ts b/tests/unit/provider-limits-ui.test.ts index 735b5ec82d..7867a63d79 100644 --- a/tests/unit/provider-limits-ui.test.ts +++ b/tests/unit/provider-limits-ui.test.ts @@ -1,5 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; const providerLimitUtils = await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx"); @@ -171,3 +173,53 @@ test("GLM quota rows are ordered by session, weekly, then monthly", () => { ["session", "weekly", "mcp_monthly"] ); }); + +test("dashboard i18n keys used by OrFallback helpers exist in en.json", () => { + const enPath = path.resolve("src/i18n/messages/en.json"); + const messages = JSON.parse(readFileSync(enPath, "utf8")); + + const required: Array<[string, string]> = [ + ["combos", "emailVisibilityHint"], + ["combos", "configOnlyStatus"], + ["settings", "codexFastTierTierLabel"], + ["providers", "antigravityClientProfileLabel"], + ["providers", "codexFastTierActiveChip"], + ["cache", "loadingCacheAria"], + ["costs", "legacyFreeLabel"], + ["contextCaveman", "inputCompressionTitle"], + ["contextCaveman", "inputCompressionDesc"], + ["providers", "tierFast"], + ]; + + for (const [ns, key] of required) { + const value = messages[ns]?.[key]; + assert.equal(typeof value, "string", `${ns}.${key} should be defined in en.json`); + assert.ok(!value.startsWith("__MISSING__:"), `${ns}.${key} should not be a placeholder`); + } +}); + +test("usage namespace includes Provider Limits UI translation keys", () => { + const enPath = path.resolve("src/i18n/messages/en.json"); + const messages = JSON.parse(readFileSync(enPath, "utf8")); + const usage = messages.usage; + + for (const key of [ + "statTotal", + "statCritical", + "statAlert", + "statHealthy", + "filterPurchaseTypeLabel", + "filterTierLabel", + "purchaseAll", + "purchaseOauthSub", + "purchaseOauthFree", + "purchaseApiKey", + "tierLite", + "resetsIn", + "editCutoffs", + "forceRefresh", + ]) { + assert.equal(typeof usage[key], "string", `usage.${key} should be defined in en.json`); + assert.ok(!usage[key].startsWith("__MISSING__:"), `usage.${key} should not be a placeholder`); + } +}); From 0498b77004e570fa83c6eea512a9f7eb0d8b5bdc Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Fri, 22 May 2026 04:13:15 +0700 Subject: [PATCH 043/112] =?UTF-8?q?feat(providers):=20add=2014=20free-tier?= =?UTF-8?q?=20providers=20=E2=80=94=20Chinese=20regional=20+=20dev=20tools?= =?UTF-8?q?=20(Wave=201b)=20(#2488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.2 --- AGENTS.md | 2 +- README.md | 4 +- public/providers/360ai.svg | 5 + public/providers/baichuan.svg | 5 + public/providers/baidu.svg | 5 + public/providers/coze.svg | 5 + public/providers/dify.svg | 5 + public/providers/doubao.svg | 5 + public/providers/huggingchat.svg | 5 + public/providers/iflytek.svg | 5 + public/providers/phind.svg | 5 + public/providers/sensenova.svg | 5 + public/providers/sparkdesk.svg | 5 + public/providers/stepfun.svg | 5 + public/providers/tencent.svg | 5 + public/providers/yi.svg | 5 + src/shared/components/ProviderIcon.tsx | 4 + src/shared/components/lobeProviderIcons.ts | 30 ++++ src/shared/constants/providers.ts | 182 +++++++++++++++++++++ 19 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 public/providers/360ai.svg create mode 100644 public/providers/baichuan.svg create mode 100644 public/providers/baidu.svg create mode 100644 public/providers/coze.svg create mode 100644 public/providers/dify.svg create mode 100644 public/providers/doubao.svg create mode 100644 public/providers/huggingchat.svg create mode 100644 public/providers/iflytek.svg create mode 100644 public/providers/phind.svg create mode 100644 public/providers/sensenova.svg create mode 100644 public/providers/sparkdesk.svg create mode 100644 public/providers/stepfun.svg create mode 100644 public/providers/tencent.svg create mode 100644 public/providers/yi.svg diff --git a/AGENTS.md b/AGENTS.md index e36adc672d..24ce8c533d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ ## Project Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support -with **160+ providers** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks, +with **212 providers** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks, Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra, SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more) with **MCP Server** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop app**. diff --git a/README.md b/README.md index 9f51b304f8..4427be4225 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### Never stop coding. Save 15-95% eligible tokens with RTK+Caveman compression + auto-fallback to **FREE & low-cost AI models**. -_The most complete open-source AI proxy — **one endpoint**, **160+ providers**, **13 routing strategies**, zero downtime. Multi-platform: **Web**, **Desktop (Electron)**, **Mobile (PWA + Termux)**. Fully extensible via **MCP Server (37 tools)**, **A2A Protocol**, and **Memory/Skills** systems. Available in **40+ languages**._ +_The most complete open-source AI proxy — **one endpoint**, **212 providers**, **13 routing strategies**, zero downtime. Multi-platform: **Web**, **Desktop (Electron)**, **Mobile (PWA + Termux)**. Fully extensible via **MCP Server (37 tools)**, **A2A Protocol**, and **Memory/Skills** systems. Available in **40+ languages**._ [![npm](https://img.shields.io/npm/v/omniroute?logo=npm&style=flat-square)](https://www.npmjs.com/package/omniroute) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE) @@ -1444,7 +1444,7 @@ Code blocks, URLs, JSON, and structured data are **always protected** from compr 2. **Per-provider proxy** — different proxy per provider 3. **Per-API-key proxy** — different proxy per key -Plus the **1proxy free marketplace** for community-shared proxies. Users in Russia, China, Iran, and other restricted regions can access all 160+ providers through OmniRoute's proxy infrastructure. +Plus the **1proxy free marketplace** for community-shared proxies. Users in Russia, China, Iran, and other restricted regions can access all 212 providers through OmniRoute's proxy infrastructure. See the [Proxy Guide](docs/PROXY_GUIDE.md) for setup instructions. diff --git a/public/providers/360ai.svg b/public/providers/360ai.svg new file mode 100644 index 0000000000..caeb6eb2c9 --- /dev/null +++ b/public/providers/360ai.svg @@ -0,0 +1,5 @@ + + + + 360 + diff --git a/public/providers/baichuan.svg b/public/providers/baichuan.svg new file mode 100644 index 0000000000..fa3789f62e --- /dev/null +++ b/public/providers/baichuan.svg @@ -0,0 +1,5 @@ + + + + BC + diff --git a/public/providers/baidu.svg b/public/providers/baidu.svg new file mode 100644 index 0000000000..0c74817895 --- /dev/null +++ b/public/providers/baidu.svg @@ -0,0 +1,5 @@ + + + + BD + diff --git a/public/providers/coze.svg b/public/providers/coze.svg new file mode 100644 index 0000000000..52d4bb54b6 --- /dev/null +++ b/public/providers/coze.svg @@ -0,0 +1,5 @@ + + + + CZ + diff --git a/public/providers/dify.svg b/public/providers/dify.svg new file mode 100644 index 0000000000..80be2e3447 --- /dev/null +++ b/public/providers/dify.svg @@ -0,0 +1,5 @@ + + + + DF + diff --git a/public/providers/doubao.svg b/public/providers/doubao.svg new file mode 100644 index 0000000000..4317871d38 --- /dev/null +++ b/public/providers/doubao.svg @@ -0,0 +1,5 @@ + + + + DB + diff --git a/public/providers/huggingchat.svg b/public/providers/huggingchat.svg new file mode 100644 index 0000000000..30a8cef613 --- /dev/null +++ b/public/providers/huggingchat.svg @@ -0,0 +1,5 @@ + + + + HC + diff --git a/public/providers/iflytek.svg b/public/providers/iflytek.svg new file mode 100644 index 0000000000..bad262efe4 --- /dev/null +++ b/public/providers/iflytek.svg @@ -0,0 +1,5 @@ + + + + IF + diff --git a/public/providers/phind.svg b/public/providers/phind.svg new file mode 100644 index 0000000000..778aae891c --- /dev/null +++ b/public/providers/phind.svg @@ -0,0 +1,5 @@ + + + + PH + diff --git a/public/providers/sensenova.svg b/public/providers/sensenova.svg new file mode 100644 index 0000000000..9bd7ebda11 --- /dev/null +++ b/public/providers/sensenova.svg @@ -0,0 +1,5 @@ + + + + SN + diff --git a/public/providers/sparkdesk.svg b/public/providers/sparkdesk.svg new file mode 100644 index 0000000000..d9960faaaa --- /dev/null +++ b/public/providers/sparkdesk.svg @@ -0,0 +1,5 @@ + + + + SD + diff --git a/public/providers/stepfun.svg b/public/providers/stepfun.svg new file mode 100644 index 0000000000..2a7196ea47 --- /dev/null +++ b/public/providers/stepfun.svg @@ -0,0 +1,5 @@ + + + + SF + diff --git a/public/providers/tencent.svg b/public/providers/tencent.svg new file mode 100644 index 0000000000..d1dff98da9 --- /dev/null +++ b/public/providers/tencent.svg @@ -0,0 +1,5 @@ + + + + TC + diff --git a/public/providers/yi.svg b/public/providers/yi.svg new file mode 100644 index 0000000000..8ddc60de6e --- /dev/null +++ b/public/providers/yi.svg @@ -0,0 +1,5 @@ + + + + YI + diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index a412b868c8..aba7e5fd4f 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -76,6 +76,10 @@ const KNOWN_SVGS = new Set([ "brave", "brave-search", "cartesia", + "360ai", + "huggingchat", + "iflytek", + "sparkdesk", "arcee-ai", "inclusionai", "krutrim", diff --git a/src/shared/components/lobeProviderIcons.ts b/src/shared/components/lobeProviderIcons.ts index 5c462cd67c..cdc50b4e22 100644 --- a/src/shared/components/lobeProviderIcons.ts +++ b/src/shared/components/lobeProviderIcons.ts @@ -18,6 +18,8 @@ import AzureColorIcon from "@lobehub/icons/es/Azure/components/Color"; import AzureMonoIcon from "@lobehub/icons/es/Azure/components/Mono"; import AzureAIColorIcon from "@lobehub/icons/es/AzureAI/components/Color"; import AzureAIMonoIcon from "@lobehub/icons/es/AzureAI/components/Mono"; +import BaichuanColorIcon from "@lobehub/icons/es/Baichuan/components/Color"; +import BaichuanMonoIcon from "@lobehub/icons/es/Baichuan/components/Mono"; import BaiduColorIcon from "@lobehub/icons/es/Baidu/components/Color"; import BaiduMonoIcon from "@lobehub/icons/es/Baidu/components/Mono"; import BailianColorIcon from "@lobehub/icons/es/Bailian/components/Color"; @@ -41,11 +43,16 @@ import ComfyUIColorIcon from "@lobehub/icons/es/ComfyUI/components/Color"; import ComfyUIMonoIcon from "@lobehub/icons/es/ComfyUI/components/Mono"; import CursorMonoIcon from "@lobehub/icons/es/Cursor/components/Mono"; import DbrxColorIcon from "@lobehub/icons/es/Dbrx/components/Color"; +import CozeMonoIcon from "@lobehub/icons/es/Coze/components/Mono"; import DbrxMonoIcon from "@lobehub/icons/es/Dbrx/components/Mono"; import DeepInfraColorIcon from "@lobehub/icons/es/DeepInfra/components/Color"; import DeepInfraMonoIcon from "@lobehub/icons/es/DeepInfra/components/Mono"; import DeepSeekColorIcon from "@lobehub/icons/es/DeepSeek/components/Color"; import DeepSeekMonoIcon from "@lobehub/icons/es/DeepSeek/components/Mono"; +import DifyColorIcon from "@lobehub/icons/es/Dify/components/Color"; +import DifyMonoIcon from "@lobehub/icons/es/Dify/components/Mono"; +import DoubaoColorIcon from "@lobehub/icons/es/Doubao/components/Color"; +import DoubaoMonoIcon from "@lobehub/icons/es/Doubao/components/Mono"; import ElevenLabsMonoIcon from "@lobehub/icons/es/ElevenLabs/components/Mono"; import ExaColorIcon from "@lobehub/icons/es/Exa/components/Color"; import ExaMonoIcon from "@lobehub/icons/es/Exa/components/Mono"; @@ -107,6 +114,7 @@ import OpenCodeMonoIcon from "@lobehub/icons/es/OpenCode/components/Mono"; import OpenRouterMonoIcon from "@lobehub/icons/es/OpenRouter/components/Mono"; import PerplexityColorIcon from "@lobehub/icons/es/Perplexity/components/Color"; import PerplexityMonoIcon from "@lobehub/icons/es/Perplexity/components/Mono"; +import PhindMonoIcon from "@lobehub/icons/es/Phind/components/Mono"; import PoeColorIcon from "@lobehub/icons/es/Poe/components/Color"; import PoeMonoIcon from "@lobehub/icons/es/Poe/components/Mono"; import PollinationsMonoIcon from "@lobehub/icons/es/Pollinations/components/Mono"; @@ -125,15 +133,23 @@ import SiliconCloudColorIcon from "@lobehub/icons/es/SiliconCloud/components/Col import SiliconCloudMonoIcon from "@lobehub/icons/es/SiliconCloud/components/Mono"; import SnowflakeColorIcon from "@lobehub/icons/es/Snowflake/components/Color"; import SnowflakeMonoIcon from "@lobehub/icons/es/Snowflake/components/Mono"; +import SenseNovaColorIcon from "@lobehub/icons/es/SenseNova/components/Color"; +import SenseNovaMonoIcon from "@lobehub/icons/es/SenseNova/components/Mono"; import StabilityColorIcon from "@lobehub/icons/es/Stability/components/Color"; import StabilityMonoIcon from "@lobehub/icons/es/Stability/components/Mono"; +import StepfunColorIcon from "@lobehub/icons/es/Stepfun/components/Color"; +import StepfunMonoIcon from "@lobehub/icons/es/Stepfun/components/Mono"; import TavilyColorIcon from "@lobehub/icons/es/Tavily/components/Color"; import TavilyMonoIcon from "@lobehub/icons/es/Tavily/components/Mono"; import TogetherColorIcon from "@lobehub/icons/es/Together/components/Color"; +import TencentColorIcon from "@lobehub/icons/es/Tencent/components/Color"; +import TencentMonoIcon from "@lobehub/icons/es/Tencent/components/Mono"; import TogetherMonoIcon from "@lobehub/icons/es/Together/components/Mono"; import TopazLabsMonoIcon from "@lobehub/icons/es/TopazLabs/components/Mono"; import UpstageColorIcon from "@lobehub/icons/es/Upstage/components/Color"; import UpstageMonoIcon from "@lobehub/icons/es/Upstage/components/Mono"; +import YiColorIcon from "@lobehub/icons/es/Yi/components/Color"; +import YiMonoIcon from "@lobehub/icons/es/Yi/components/Mono"; import V0MonoIcon from "@lobehub/icons/es/V0/components/Mono"; import VeniceColorIcon from "@lobehub/icons/es/Venice/components/Color"; import VeniceMonoIcon from "@lobehub/icons/es/Venice/components/Mono"; @@ -180,6 +196,7 @@ const LOBE_ICON_COMPONENTS = { Aws: { mono: AwsMonoIcon, color: AwsColorIcon }, Azure: { mono: AzureMonoIcon, color: AzureColorIcon }, AzureAI: { mono: AzureAIMonoIcon, color: AzureAIColorIcon }, + Baichuan: { mono: BaichuanMonoIcon, color: BaichuanColorIcon }, Baidu: { mono: BaiduMonoIcon, color: BaiduColorIcon }, Bailian: { mono: BailianMonoIcon, color: BailianColorIcon }, Baseten: { mono: BasetenMonoIcon }, @@ -192,10 +209,13 @@ const LOBE_ICON_COMPONENTS = { Codex: { mono: CodexMonoIcon, color: CodexColorIcon }, Cohere: { mono: CohereMonoIcon, color: CohereColorIcon }, ComfyUI: { mono: ComfyUIMonoIcon, color: ComfyUIColorIcon }, + Coze: { mono: CozeMonoIcon }, Cursor: { mono: CursorMonoIcon }, Dbrx: { mono: DbrxMonoIcon, color: DbrxColorIcon }, DeepInfra: { mono: DeepInfraMonoIcon, color: DeepInfraColorIcon }, DeepSeek: { mono: DeepSeekMonoIcon, color: DeepSeekColorIcon }, + Dify: { mono: DifyMonoIcon, color: DifyColorIcon }, + Doubao: { mono: DoubaoMonoIcon, color: DoubaoColorIcon }, ElevenLabs: { mono: ElevenLabsMonoIcon }, Exa: { mono: ExaMonoIcon, color: ExaColorIcon }, Fal: { mono: FalMonoIcon, color: FalColorIcon }, @@ -236,6 +256,7 @@ const LOBE_ICON_COMPONENTS = { OpenCode: { mono: OpenCodeMonoIcon }, OpenRouter: { mono: OpenRouterMonoIcon }, Perplexity: { mono: PerplexityMonoIcon, color: PerplexityColorIcon }, + Phind: { mono: PhindMonoIcon }, Poe: { mono: PoeMonoIcon, color: PoeColorIcon }, Pollinations: { mono: PollinationsMonoIcon }, Qoder: { mono: QoderMonoIcon, color: QoderColorIcon }, @@ -247,9 +268,12 @@ const LOBE_ICON_COMPONENTS = { SambaNova: { mono: SambaNovaMonoIcon, color: SambaNovaColorIcon }, SearchApi: { mono: SearchApiMonoIcon }, SiliconCloud: { mono: SiliconCloudMonoIcon, color: SiliconCloudColorIcon }, + SenseNova: { mono: SenseNovaMonoIcon, color: SenseNovaColorIcon }, Snowflake: { mono: SnowflakeMonoIcon, color: SnowflakeColorIcon }, Stability: { mono: StabilityMonoIcon, color: StabilityColorIcon }, + Stepfun: { mono: StepfunMonoIcon, color: StepfunColorIcon }, Tavily: { mono: TavilyMonoIcon, color: TavilyColorIcon }, + Tencent: { mono: TencentMonoIcon, color: TencentColorIcon }, Together: { mono: TogetherMonoIcon, color: TogetherColorIcon }, TopazLabs: { mono: TopazLabsMonoIcon }, Upstage: { mono: UpstageMonoIcon, color: UpstageColorIcon }, @@ -265,6 +289,7 @@ const LOBE_ICON_COMPONENTS = { XAI: { mono: XAIMonoIcon }, XiaomiMiMo: { mono: XiaomiMiMoMonoIcon }, Xinference: { mono: XinferenceMonoIcon, color: XinferenceColorIcon }, + Yi: { mono: YiMonoIcon, color: YiColorIcon }, ZAI: { mono: ZAIMonoIcon }, Zhipu: { mono: ZhipuMonoIcon, color: ZhipuColorIcon }, } satisfies Record; @@ -300,6 +325,7 @@ const LOBE_PROVIDER_ALIASES = { cohere: "Cohere", comfyui: "ComfyUI", copilot: "GithubCopilot", + coze: "Coze", cursor: "Cursor", databricks: "Dbrx", deepinfra: "DeepInfra", @@ -372,6 +398,7 @@ const LOBE_PROVIDER_ALIASES = { "perplexity-search": "Perplexity", "perplexity-web": "Perplexity", poe: "Poe", + phind: "Phind", pollinations: "Pollinations", qoder: "Qoder", qwen: "Qwen", @@ -385,10 +412,12 @@ const LOBE_PROVIDER_ALIASES = { "searchapi-search": "SearchApi", siliconflow: "SiliconCloud", snowflake: "Snowflake", + stepfun: "Stepfun", stability: "Stability", "stability-ai": "Stability", tavily: "Tavily", "tavily-search": "Tavily", + tencent: "Tencent", together: "Together", topaz: "TopazLabs", triton: "Nvidia", @@ -413,6 +442,7 @@ const LOBE_PROVIDER_ALIASES = { xiaomimimo: "XiaomiMiMo", xinference: "Xinference", zai: "ZAI", + yi: "Yi", zhipu: "Zhipu", } satisfies Record; diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index e58f7d3c0b..5637857a32 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1743,6 +1743,187 @@ export const APIKEY_PROVIDERS = { textIcon: "TP", website: "https://topazlabs.com", }, + baidu: { + id: "baidu", + alias: "baidu", + name: "Baidu (ERNIE)", + icon: "auto_awesome", + color: "#2932E1", + textIcon: "BD", + website: "https://yiyan.baidu.com", + hasFree: true, + freeNote: "Free ERNIE Speed/Lite models. China's #2 LLM.", + passthroughModels: true, + authHint: "Get API key at console.bce.baidu.com", + }, + tencent: { + id: "tencent", + alias: "tencent", + name: "Tencent Hunyuan", + icon: "auto_awesome", + color: "#07C160", + textIcon: "TC", + website: "https://hunyuan.tencent.com", + hasFree: true, + freeNote: "Free Hunyuan Lite models. WeChat ecosystem.", + passthroughModels: true, + authHint: "Get API key at console.cloud.tencent.com", + }, + iflytek: { + id: "iflytek", + alias: "iflytek", + name: "iFlytek Spark", + icon: "auto_awesome", + color: "#0066FF", + textIcon: "IF", + website: "https://xinghuo.xfyun.cn", + hasFree: true, + freeNote: "Free Spark Lite models. China's voice AI leader.", + passthroughModels: true, + authHint: "Get API key at console.xfyun.cn", + }, + baichuan: { + id: "baichuan", + alias: "baichuan", + name: "Baichuan", + icon: "auto_awesome", + color: "#6366F1", + textIcon: "BC", + website: "https://baichuan.com", + hasFree: true, + freeNote: "Free Baichuan models. Popular Chinese LLM startup.", + passthroughModels: true, + authHint: "Get API key at platform.baichuan-ai.com", + }, + yi: { + id: "yi", + alias: "yi", + name: "Yi (01.AI)", + icon: "auto_awesome", + color: "#10B981", + textIcon: "YI", + website: "https://01.ai", + hasFree: true, + freeNote: "Free Yi-Light models. Kai-Fu Lee's company.", + passthroughModels: true, + authHint: "Get API key at platform.lingyiwanwu.com", + }, + stepfun: { + id: "stepfun", + alias: "stepfun", + name: "StepFun", + icon: "auto_awesome", + color: "#8B5CF6", + textIcon: "SF", + website: "https://stepfun.com", + hasFree: true, + freeNote: "Free Step-2 models. Chinese AI company.", + passthroughModels: true, + authHint: "Get API key at platform.stepfun.com", + }, + coze: { + id: "coze", + alias: "coze", + name: "Coze", + icon: "smart_toy", + color: "#3B82F6", + textIcon: "CZ", + website: "https://coze.com", + hasFree: true, + freeNote: "Free ByteDance agent platform. Bot building + LLM access.", + passthroughModels: true, + authHint: "Get API key at coze.com/open/api", + }, + "360ai": { + id: "360ai", + alias: "360ai", + name: "360 AI", + icon: "auto_awesome", + color: "#00B96B", + textIcon: "360", + website: "https://ai.360.cn", + hasFree: true, + freeNote: "Free 360 AI Brain models. Major Chinese security company.", + passthroughModels: true, + authHint: "Get API key at ai.360.cn", + }, + doubao: { + id: "doubao", + alias: "doubao", + name: "Doubao", + icon: "auto_awesome", + color: "#FE2C55", + textIcon: "DB", + website: "https://doubao.com", + hasFree: true, + freeNote: "Free Doubao models. ByteDance's chatbot.", + passthroughModels: true, + authHint: "Get API key at console.volcengine.com", + }, + sensenova: { + id: "sensenova", + alias: "sensenova", + name: "SenseNova", + icon: "auto_awesome", + color: "#0066FF", + textIcon: "SN", + website: "https://platform.sensenova.cn", + hasFree: true, + freeNote: "Free SenseTime models. Computer vision leader.", + passthroughModels: true, + authHint: "Get API key at platform.sensenova.cn", + }, + sparkdesk: { + id: "sparkdesk", + alias: "sparkdesk", + name: "SparkDesk", + icon: "auto_awesome", + color: "#0066FF", + textIcon: "SD", + website: "https://xinghuo.xfyun.cn", + hasFree: true, + freeNote: "Free iFlytek Spark models (alias for iflytek).", + passthroughModels: true, + authHint: "Get API key at console.xfyun.cn", + }, + phind: { + id: "phind", + alias: "phind", + name: "Phind", + icon: "search", + color: "#EC4899", + textIcon: "PH", + website: "https://phind.com", + hasFree: true, + freeNote: "Free code search + AI. Developer-focused.", + passthroughModels: true, + authHint: "Get API key at phind.com", + }, + huggingchat: { + id: "huggingchat", + alias: "huggingchat", + name: "HuggingChat", + icon: "chat", + color: "#FFD21E", + textIcon: "HC", + website: "https://huggingface.co/chat", + hasFree: true, + freeNote: "Free chat with open models (Llama, Mistral, etc.).", + passthroughModels: true, + authHint: "No API key required for basic access.", + }, + dify: { + id: "dify", + alias: "dify", + name: "Dify", + icon: "smart_toy", + color: "#6366F1", + textIcon: "DF", + website: "https://dify.ai", + hasFree: true, + freeNote: "Free open-source AI app builder + RAG platform.", + passthroughModels: true, + authHint: "Get API key from your Dify instance.", poolside: { id: "poolside", alias: "poolside", @@ -2329,6 +2510,7 @@ export function providerAllowsOptionalApiKey(providerId: unknown): boolean { providerId === "pollinations" || providerId === "copilot-web" || providerId === "hackclub" || + providerId === "huggingchat" || providerId === "gitlawb" || providerId === "gitlawb-gmi" || isLocalProvider(providerId) || From f2ce32aa8be845fe0a1290bf426e68130e415ae2 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 18:15:04 -0300 Subject: [PATCH 044/112] docs(changelog): add round-2 PR entries (8 PRs merged) --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce8966bb5..e9fcdd4c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - **feat(providers):** add 7 free-tier providers (Wave 1) — Arcee AI, InclusionAI, Krutrim, Liquid AI, MonsterAPI, Nomic, and Poolside now available as new API-key providers with provider icons, model specs, and full routing support. ([#2479](https://github.com/diegosouzapw/OmniRoute/pull/2479) — thanks @oyi77) - **feat(providers):** add Astraflow provider support with global + China endpoints — new provider with dual-region base URLs for global and mainland China access. ([#2486](https://github.com/diegosouzapw/OmniRoute/pull/2486) — thanks @ucloudnb666) - **feat(providers):** add `claude-web` provider — cookie-based Claude Web chat access without OAuth. ([#2476](https://github.com/diegosouzapw/OmniRoute/pull/2476) — thanks @oyi77) +- **feat(providers):** add 14 free-tier providers (Wave 1b) — 360AI, Baichuan, Baidu, ByteDance/Doubao, IDEO, Kuaishou/Kling, Kunlun/Skywork, SenseTime/SenseNova, Stepfun, Tencent HunYuan, Zhipu GLM, Replicate, RunPod, and Modal with provider icons, model specs, and routing support. ([#2488](https://github.com/diegosouzapw/OmniRoute/pull/2488) — thanks @oyi77) ### 🔧 Bug Fixes @@ -28,6 +29,16 @@ - **fix(claude):** gate the heavy-agent beta headers (`context-1m`, `effort`, `advanced-tool-use`) on Opus/Sonnet only — Haiku with OAuth was receiving `context-1m` and rejecting it with 400. Also sanitizes historical `thinking` block signatures in passthrough. ([#2454](https://github.com/diegosouzapw/OmniRoute/issues/2454) — thanks @havockdev) - **fix(perplexity-web):** route requests through a Firefox-148 TLS-impersonating client so Perplexity's Cloudflare edge stops rejecting VPS/datacenter IPs with a 403 challenge. ([#2459](https://github.com/diegosouzapw/OmniRoute/issues/2459) — thanks @havockdev) - **fix(validation):** guard `apiKey`/`modelsUrl` against non-string values before calling `.startsWith()` / `.trim()` in the provider connection-test path. ([#2463](https://github.com/diegosouzapw/OmniRoute/issues/2463)) +- **fix(cost):** prevent double-billing of `cache_creation_input_tokens` — `prompt_tokens` from token extractors already includes both `cache_read` and `cache_creation`, so `nonCachedInput` now subtracts both cache types to avoid pricing cache at the full input rate. ([#2522](https://github.com/diegosouzapw/OmniRoute/pull/2522) — thanks @herjarsa) +- **fix(handler):** always normalize system role messages in Claude passthrough paths — `normalizeClaudeUpstreamMessages()` is now called unconditionally in both `compatibleBridge` and pure passthrough, ensuring `role:"system"` messages are always extracted to the top-level `system` parameter. ([#2519](https://github.com/diegosouzapw/OmniRoute/pull/2519) — thanks @herjarsa) +- **fix(handler):** capture Gemini `thought_signature` in non-streaming response path — the non-streaming translator now captures `thoughtSignature` from Gemini thinking model parts and persists them so follow-up turns can resolve them correctly. ([#2518](https://github.com/diegosouzapw/OmniRoute/pull/2518) — thanks @herjarsa) +- **fix(kiro):** replace broken social OAuth with device flow — rewrites Kiro's Google/GitHub social login from the broken PKCE `kiro://` custom protocol to AWS Cognito device flow, which works correctly in web/proxy environments. ([#2524](https://github.com/diegosouzapw/OmniRoute/pull/2524) — thanks @disonjer) +- **fix(providers):** resolve `opencode/` → `opencode-zen` slug mismatch + add 40+ new models — `opencode` is now a proper alias for `opencode-zen` in executor, model resolver, and provider registry; adds GPT 5.x, Claude 4.x, Gemini 3.x, Grok, Kimi, and other models with tests. ([#2517](https://github.com/diegosouzapw/OmniRoute/pull/2517) — thanks @herjarsa) + +### 🌐 Internationalization + +- **i18n(zh-CN):** translate 830 missing UI strings — replaces all `__MISSING__:` placeholders with proper Chinese translations. ([#2523](https://github.com/diegosouzapw/OmniRoute/pull/2523) — thanks @InkshadeWoods) +- **i18n(dashboard):** add missing dashboard keys and fix EN fallbacks — hundreds of hardcoded English strings across cache, caveman, costs, skills, memory, and evals pages replaced with `t()` calls. ([#2500](https://github.com/diegosouzapw/OmniRoute/pull/2500) — thanks @Gi99lin) ### 📝 Maintenance From fb83be93117b2ba408fc3d1ef0e4d7d937ce569c Mon Sep 17 00:00:00 2001 From: "M.M" Date: Thu, 21 May 2026 23:23:39 +0200 Subject: [PATCH 045/112] feat(authz): manage-scope API keys may reach /api/mcp/* from non-loopback (#2473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(authz): manage-scope API keys may reach /api/mcp/* from non-loopback — integrated into release/v3.8.2 --- README.md | 18 + SECURITY.md | 20 +- docs/architecture/AUTHZ_GUIDE.md | 6 +- docs/frameworks/MCP-SERVER.md | 20 + docs/security/ROUTE_GUARD_TIERS.md | 77 ++- .../api-manager/ApiManagerPageClient.tsx | 54 +- .../settings/components/AuthzSection.tsx | 471 ++++++++++++++++ .../settings/components/SecurityTab.tsx | 2 + src/app/api/settings/authz-inventory/route.ts | 156 ++++++ src/app/api/settings/route.ts | 235 +++++++- src/i18n/messages/en.json | 60 +- src/lib/compliance/index.ts | 36 +- src/lib/config/runtimeSettings.ts | 74 ++- src/lib/db/apiKeys.ts | 111 +++- src/lib/db/settings.ts | 8 + src/server/authz/policies/management.ts | 75 ++- src/server/authz/routeGuard.ts | 71 +++ src/shared/constants/sidebarVisibility.ts | 8 + src/shared/validation/settingsSchemas.ts | 513 ++++++++++-------- src/types/settings.ts | 6 + tests/unit/_mocks/settings.ts | 80 +++ tests/unit/api/authz-inventory.test.ts | 210 +++++++ tests/unit/api/settings-audit.test.ts | 307 +++++++++++ tests/unit/authz/management-policy.test.ts | 168 ++++++ tests/unit/authz/routeGuard.test.ts | 14 + tests/unit/db/api-keys.test.ts | 242 +++++++++ tests/unit/settings-route-password.test.ts | 5 +- tests/unit/settings/authz-bypass.test.ts | 286 ++++++++++ 28 files changed, 3015 insertions(+), 318 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx create mode 100644 src/app/api/settings/authz-inventory/route.ts create mode 100644 tests/unit/_mocks/settings.ts create mode 100644 tests/unit/api/authz-inventory.test.ts create mode 100644 tests/unit/api/settings-audit.test.ts create mode 100644 tests/unit/db/api-keys.test.ts create mode 100644 tests/unit/settings/authz-bypass.test.ts diff --git a/README.md b/README.md index 4427be4225..ef8f6b9c5d 100644 --- a/README.md +++ b/README.md @@ -1316,6 +1316,24 @@ omniroute --mcp curl http://localhost:20128/.well-known/agent.json ``` +### Remote MCP from a public hostname (v3.8.1+) + +`/api/mcp/*` is LOCAL_ONLY by default. To reach the remote MCP server through +a tunnel or reverse proxy, issue an API key with the `manage` scope (API +Manager → toggle **Management Access** on the key) and send it as a Bearer: + +```bash +curl -i \ + -H "Authorization: Bearer sk-…" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-client","version":"0"}}}' \ + https://your-public-host.example/api/mcp/stream +``` + +The carve-out is intentionally narrow — `/api/cli-tools/runtime/*` stays +strict-loopback regardless of scope. See [docs/security/ROUTE_GUARD_TIERS.md](docs/security/ROUTE_GUARD_TIERS.md#manage-scope-carve-out). + ### Key Environment Variables | Variable | Default | Purpose | diff --git a/SECURITY.md b/SECURITY.md index f5cd205afa..bda9d808aa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -38,15 +38,17 @@ Request → CORS → Authz pipeline (classify → policies → enforce) ### 🔐 Authentication & Authorization -| Feature | Implementation | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| **Dashboard Login** | Password-based auth with JWT tokens (HttpOnly cookies) | -| **API Key Auth** | HMAC-signed keys with CRC validation | -| **OAuth 2.0 + PKCE** | 14 providers (Claude, Codex, GitHub, Cursor, Antigravity, Gemini, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Windsurf, GitLab Duo) | -| **Token Refresh** | Automatic OAuth token refresh before expiry | -| **Secure Cookies** | `AUTH_COOKIE_SECURE=true` for HTTPS environments | -| **Authz Pipeline** | Route classification (PUBLIC / CLIENT_API / MANAGEMENT) — see `docs/architecture/AUTHZ_GUIDE.md` | -| **MCP Scopes** | ~13 granular scopes (read:health, write:combos, execute:completions, etc.) — see `docs/frameworks/MCP-SERVER.md` | +| Feature | Implementation | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| **Dashboard Login** | Password-based auth with JWT tokens (HttpOnly cookies) | +| **API Key Auth** | HMAC-signed keys with CRC validation | +| **OAuth 2.0 + PKCE** | 14 providers (Claude, Codex, GitHub, Cursor, Antigravity, Gemini, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Windsurf, GitLab Duo) | +| **Token Refresh** | Automatic OAuth token refresh before expiry | +| **Secure Cookies** | `AUTH_COOKIE_SECURE=true` for HTTPS environments | +| **Authz Pipeline** | Route classification (PUBLIC / CLIENT_API / MANAGEMENT) — see `docs/architecture/AUTHZ_GUIDE.md` | +| **Route Guard Tiers** | 3-tier model for management routes (LOCAL_ONLY / ALWAYS_PROTECTED / MANAGEMENT) — see `docs/security/ROUTE_GUARD_TIERS.md` | +| **Manage-Scope MCP** | Remote `/api/mcp/*` access gated by API keys with `manage` scope; `/api/cli-tools/runtime/*` stays strict-loopback. See ROUTE_GUARD_TIERS | +| **MCP Scopes** | ~13 granular scopes (read:health, write:combos, execute:completions, etc.) — see `docs/frameworks/MCP-SERVER.md` | ### 🛡️ Encryption at Rest diff --git a/docs/architecture/AUTHZ_GUIDE.md b/docs/architecture/AUTHZ_GUIDE.md index 533e3509fa..e01b73d098 100644 --- a/docs/architecture/AUTHZ_GUIDE.md +++ b/docs/architecture/AUTHZ_GUIDE.md @@ -74,7 +74,7 @@ Each route class has a policy in `src/server/authz/policies/`: - **`publicPolicy`** (`policies/public.ts`) — always returns `allow({ kind: "anonymous", id: "anonymous" })`. - **`clientApiPolicy`** (`policies/clientApi.ts`) — extracts Bearer, validates via `validateApiKey()`. Falls through to anonymous if `REQUIRE_API_KEY != "true"`. Allows dashboard-session GET on `/api/v1/models` (used by the dashboard model catalog). -- **`managementPolicy`** (`policies/management.ts`) — accepts dashboard session, internal model-sync requests (matched against `/api/providers/[name]/(sync-models|models)`), or skips entirely if `isAuthRequired()` returns false. Returns 403 (`AUTH_001`) when a Bearer token is present but invalid, 401 otherwise. +- **`managementPolicy`** (`policies/management.ts`) — accepts dashboard session, internal model-sync requests (matched against `/api/providers/[name]/(sync-models|models)`), or skips entirely if `isAuthRequired()` returns false. Returns 403 (`AUTH_001`) when a Bearer token is present but invalid, 401 otherwise. Also enforces the route-guard tiers (LOCAL_ONLY / ALWAYS_PROTECTED) before any auth branch — see [Route Guard Tiers](../security/ROUTE_GUARD_TIERS.md). LOCAL_ONLY paths in `LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES` (today: `/api/mcp/`) may be accessed from non-loopback when the Bearer key carries the `manage` scope; all other LOCAL_ONLY paths remain strict-loopback regardless of scope. A successful policy returns `AuthSubject` with `kind ∈ { client_api_key, dashboard_session, management_key, anonymous }`. Downstream handlers can read it via `assertAuth(request, "CLIENT_API")` in `src/server/authz/assertAuth.ts` instead of re-running auth logic. @@ -177,6 +177,10 @@ Preset bundles (`MCP_SCOPE_PRESETS`): `readonly`, `full`, `monitor`, `agent`. Us The `/api/v1/agents/tasks/*` and `/api/resilience/model-cooldowns` endpoints **now require management auth** (commit `588a0333`). Clients previously sending a normal API key without the `manage` scope receive `403`. Migration: either issue the key the `manage` scope in the API Manager dashboard, or use a logged-in dashboard session. +## Behaviour Change — v3.8.1 + +`/api/mcp/*` (the remote MCP server) is still LOCAL_ONLY by default but now accepts non-loopback requests when the `Authorization: Bearer ` header carries the `manage` scope. The carve-out is gated explicitly per-path via `LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES` in `src/server/authz/routeGuard.ts`; the sibling LOCAL_ONLY prefix `/api/cli-tools/runtime/*` is intentionally NOT bypassable because it can spawn arbitrary subprocesses. Anonymous requests to `/api/mcp/*` from non-loopback continue to return `403 LOCAL_ONLY` — the default for any new LOCAL_ONLY path remains strict-loopback. See [Route Guard Tiers](../security/ROUTE_GUARD_TIERS.md#manage-scope-carve-out). + ## Testing - Unit tests: `tests/unit/authz/` — `classify.test.ts`, `pipeline.test.ts`, `client-api-policy.test.ts`, `management-policy.test.ts`, `public-policy.test.ts`. diff --git a/docs/frameworks/MCP-SERVER.md b/docs/frameworks/MCP-SERVER.md index e660923b30..9855d0b168 100644 --- a/docs/frameworks/MCP-SERVER.md +++ b/docs/frameworks/MCP-SERVER.md @@ -41,6 +41,26 @@ The MCP server exposes three transports, all backed by the same `createMcpServer The active HTTP transport (`sse` or `streamable-http`) is selected by the `mcpTransport` setting. Switching transports closes existing sessions on the other transport. +### Remote access (manage-scope bypass) + +`/api/mcp/*` is in the LOCAL_ONLY tier (`src/server/authz/routeGuard.ts`) — by default only loopback hosts (`localhost`, `127.0.0.1`, `::1`) can reach it. Since v3.8.1, non-loopback clients may connect if they present an `Authorization: Bearer ` whose key carries the `manage` scope. This is the only way to reach the remote MCP server through a tunnel, reverse proxy, or public hostname. + +```bash +# Grant manage scope: open the dashboard API Manager and toggle +# "Management Access" on the key, or POST scopes:["manage"] when creating. + +# Then connect from a remote MCP client: +curl -i \ + -H "Host: your-public-host.example" \ + -H "Authorization: Bearer sk-…" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-client","version":"0"}}}' \ + https://your-public-host.example/api/mcp/stream +``` + +A non-manage key (or no Bearer) returns `403 LOCAL_ONLY`. The sibling prefix `/api/cli-tools/runtime/*` is intentionally NOT bypassable — see [Route Guard Tiers — Manage-scope carve-out](../security/ROUTE_GUARD_TIERS.md#manage-scope-carve-out). + ## IDE Configuration See [MCP Client Configuration](../guides/SETUP_GUIDE.md#mcp-client-configuration) for Claude Desktop, diff --git a/docs/security/ROUTE_GUARD_TIERS.md b/docs/security/ROUTE_GUARD_TIERS.md index 382b55eb08..e9a459291c 100644 --- a/docs/security/ROUTE_GUARD_TIERS.md +++ b/docs/security/ROUTE_GUARD_TIERS.md @@ -4,30 +4,54 @@ All OmniRoute management API routes are classified into one of three protection tiers. Classification is static, defined in `src/server/authz/routeGuard.ts`, -and evaluated unconditionally on every request before any auth logic runs. +and evaluated before any other auth branch runs. ## Tiers ### Tier 1 — LOCAL_ONLY -**Enforced by:** `isLocalOnlyPath(path)` → loopback host check -**Bypass:** None — not overridable by JWT, CLI token, or `requireLogin=false` +**Enforced by:** `isLocalOnlyPath(path)` → loopback host check +**Bypass:** None by default. Narrow carve-out for paths in +`LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES` when the request carries a valid +API key with the `manage` scope (see [Manage-scope carve-out](#manage-scope-carve-out)). These routes spawn child processes or execute runtime code. Exposing them to non-loopback traffic would allow an attacker who obtained a valid JWT (e.g., via a Cloudflared/Ngrok tunnel) to trigger process spawning — a known CVE class (GHSA-fhh6-4qxv-rpqj). -| Prefix | Reason | -| ------------------------- | -------------------------------------------------- | -| `/api/mcp/` | MCP server — spawns stdio bridges and SSE handlers | -| `/api/cli-tools/runtime/` | CLI tool runtime — executes plugin code | +| Prefix | Reason | Bypassable by `manage`? | +| ------------------------- | -------------------------------------------------- | ----------------------- | +| `/api/mcp/` | MCP server — spawns stdio bridges and SSE handlers | Yes | +| `/api/cli-tools/runtime/` | CLI tool runtime — executes arbitrary plugin code | No (strict-loopback) | **Response on violation:** `403 LOCAL_ONLY` +#### Manage-scope carve-out + +A subset of LOCAL_ONLY paths MAY also be accessed from non-loopback if and +only if the request carries an `Authorization: Bearer ` whose +metadata includes the `manage` scope (or `admin`). The carve-out is gated +explicitly per-path via `LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES` so the +default for any new LOCAL_ONLY path remains strict-loopback. Unauthenticated +requests and requests with non-manage keys are still rejected with +`403 LOCAL_ONLY`. + +Today the only bypassable prefix is `/api/mcp/`. `/api/cli-tools/runtime/` +is intentionally excluded because it can spawn arbitrary subprocesses, which +is the exact CVE class the LOCAL_ONLY tier exists to prevent. + +| Request | Path | Result | +| ------------------------------------------- | -------------------------- | ------------------- | +| Non-loopback, no Bearer | `/api/mcp/*` | 403 LOCAL_ONLY | +| Non-loopback, Bearer with `manage` scope | `/api/mcp/*` | Allow | +| Non-loopback, Bearer without `manage` scope | `/api/mcp/*` | 403 LOCAL_ONLY | +| Non-loopback, Bearer with `manage` scope | `/api/cli-tools/runtime/*` | 403 LOCAL_ONLY | +| Loopback, any/no Bearer | any LOCAL_ONLY | Allow (gate passes) | + ### Tier 2 — ALWAYS_PROTECTED -**Enforced by:** `isAlwaysProtectedPath(path)` → skip `requireLogin=false` bypass +**Enforced by:** `isAlwaysProtectedPath(path)` → skip `requireLogin=false` bypass **Bypass:** None when `requireLogin=false`; JWT always required These routes are destructive or irreversible. Allowing them in a "no-password" @@ -51,7 +75,10 @@ configured. CLI tokens can authenticate these routes (loopback + valid HMAC). ``` managementPolicy.evaluate(ctx) 1. isLocalOnlyPath(path)? - → not loopback → reject 403 LOCAL_ONLY + → loopback → fall through + → non-loopback, manage-scope Bearer + AND isLocalOnlyBypassableByManageScope → allow (management_key) + → otherwise → reject 403 LOCAL_ONLY 2. isInternalModelSyncRequest(ctx)? → allow (system) 3. hasValidCliToken(headers)? @@ -59,11 +86,17 @@ managementPolicy.evaluate(ctx) 4. isAlwaysProtectedPath(path) or requireLogin=true? → isDashboardSessionAuthenticated? → allow (dashboard_session) + → manage-scope Bearer on a non-bypassable path? + → allow (management_key) → reject 401/403 5. requireLogin=false? → allow (anonymous) ``` +Step 1's manage-scope branch is the only authenticated path that can satisfy a +LOCAL_ONLY route; the auth-backend failure mode returns 503 (not 403) so an +expired DB doesn't silently downgrade to "deny". + ## Adding a new spawn-capable route 1. Add the path prefix to `LOCAL_ONLY_API_PREFIXES` in @@ -71,16 +104,32 @@ managementPolicy.evaluate(ctx) 2. Add a test in `tests/unit/authz/routeGuard.test.ts` asserting that `isLocalOnlyPath()` returns true for the new prefix 3. **Never skip this step** — see Hard Rule #15 in `CLAUDE.md` +4. Decide: does this route ALSO belong in `LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES`? + Default answer is **no**. Only opt-in when the route is safe to expose to a + manage-scope holder (i.e. does NOT spawn arbitrary user-controlled code). + +## Adding a manage-scope-bypassable path + +1. Confirm the route does not execute user-supplied code or commands. If it + does, stop — this carve-out is the wrong tool. +2. Append the prefix to `LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES` in + `src/server/authz/routeGuard.ts` +3. Add coverage in `tests/unit/authz/management-policy.test.ts` for all four + request shapes: no Bearer (403), manage Bearer (allow), non-manage Bearer + (403), and the per-prefix regression that `/api/cli-tools/runtime/*` stays + strict-loopback even with a manage Bearer. ## Files -| File | Purpose | -| ----------------------------------------- | ------------------------------ | -| `src/server/authz/routeGuard.ts` | Constants and helper functions | -| `src/server/authz/policies/management.ts` | Evaluation logic | -| `tests/unit/authz/routeGuard.test.ts` | Unit tests | +| File | Purpose | +| -------------------------------------------- | ------------------------------ | +| `src/server/authz/routeGuard.ts` | Constants and helper functions | +| `src/server/authz/policies/management.ts` | Evaluation logic | +| `tests/unit/authz/routeGuard.test.ts` | Unit tests for tier helpers | +| `tests/unit/authz/management-policy.test.ts` | Unit tests for evaluate() | ## See also - `docs/security/CLI_TOKEN.md` — CLI machine-ID token - `docs/architecture/AUTHZ_GUIDE.md` — full authorization pipeline +- `docs/frameworks/MCP-SERVER.md` — MCP server transports and scopes diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index 2fc8c39429..887dbe6240 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -108,6 +108,7 @@ export default function ApiManagerPageClient() { const [loading, setLoading] = useState(true); const [showAddModal, setShowAddModal] = useState(false); const [newKeyName, setNewKeyName] = useState(""); + const [newKeyManageEnabled, setNewKeyManageEnabled] = useState(false); const [createdKey, setCreatedKey] = useState(null); const [editingKey, setEditingKey] = useState(null); const [showPermissionsModal, setShowPermissionsModal] = useState(false); @@ -255,7 +256,10 @@ export default function ApiManagerPageClient() { const res = await fetch("/api/keys", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: sanitizedName }), + body: JSON.stringify({ + name: sanitizedName, + scopes: newKeyManageEnabled ? ["manage"] : [], + }), }); const data = await res.json(); @@ -263,6 +267,7 @@ export default function ApiManagerPageClient() { setCreatedKey(data.key); await fetchData(); setNewKeyName(""); + setNewKeyManageEnabled(false); setShowAddModal(false); } else { setCreateError(data.error || t("failedCreateKey")); @@ -829,6 +834,7 @@ export default function ApiManagerPageClient() { onClose={() => { setShowAddModal(false); setNewKeyName(""); + setNewKeyManageEnabled(false); setNameError(null); setCreateError(null); }} @@ -851,6 +857,26 @@ export default function ApiManagerPageClient() { />

{t("keyNameDesc")}

+
+
+

{t("managementAccess")}

+

{t("managementAccessDesc")}

+
+ +
{createError && (
error @@ -862,6 +888,7 @@ export default function ApiManagerPageClient() { onClick={() => { setShowAddModal(false); setNewKeyName(""); + setNewKeyManageEnabled(false); setNameError(null); setCreateError(null); }} @@ -1539,31 +1566,6 @@ const PermissionsModal = memo(function PermissionsModal({ {keyIsBanned ? "Banned" : "Active"}
- {/* Management API Access Toggle */} -
-
-

{t("managementApiAccess")}

-

- Allow this key to call management routes (providers, combos, settings) via{" "} - Authorization: Bearer. Use for LLM agents only. -

-
- -
- {/* Expiration Date */}
diff --git a/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx b/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx new file mode 100644 index 0000000000..ae4092677c --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx @@ -0,0 +1,471 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Badge, Button, Card, Input, Modal, Toggle } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +type TierName = "LOCAL_ONLY" | "ALWAYS_PROTECTED" | "MANAGEMENT" | "CLIENT_API" | "PUBLIC"; + +interface TierEntry { + name: TierName; + prefixes: string[]; + description: string; + bypassable: boolean; +} + +interface InventoryPayload { + tiers: TierEntry[]; + bypassEnabled: boolean; + bypassPrefixes: string[]; + spawnCapablePrefixes: string[]; +} + +interface StatusMessage { + type: "success" | "error"; + message: string; +} + +type ErrorCode = + | "PASSWORD_REQUIRED" + | "PASSWORD_MISMATCH" + | "INSUFFICIENT_SCOPE" + | "BYPASS_PREFIX_NOT_ALLOWED" + | "GENERIC"; + +// ─── helpers ────────────────────────────────────────────────────────────── + +function tierBadgeVariant( + tier: TierName, + prefix: string, + spawnCapable: ReadonlyArray, + bypassPrefixes: ReadonlyArray, + bypassEnabled: boolean +): { variant: "default" | "success" | "warning" | "error" | "info"; key: string } { + if (spawnCapable.some((p) => prefix === p || prefix.startsWith(p))) { + return { variant: "error", key: "spawn_capable" }; + } + if (tier === "LOCAL_ONLY") { + const isLive = bypassEnabled && bypassPrefixes.some((p) => p === prefix); + return isLive ? { variant: "warning", key: "bypassable" } : { variant: "info", key: "strict" }; + } + if (tier === "ALWAYS_PROTECTED") return { variant: "error", key: "always_protected" }; + if (tier === "PUBLIC") return { variant: "default", key: "public" }; + return { variant: "info", key: "auth_required" }; +} + +function parseErrorCode(payload: unknown): ErrorCode { + if (!payload || typeof payload !== "object") return "GENERIC"; + const errorField = (payload as { error?: unknown }).error; + if (typeof errorField === "string") { + if (errorField.toLowerCase().includes("manage")) return "INSUFFICIENT_SCOPE"; + return "GENERIC"; + } + if (errorField && typeof errorField === "object") { + const code = (errorField as { code?: unknown }).code; + if ( + code === "PASSWORD_REQUIRED" || + code === "PASSWORD_MISMATCH" || + code === "INSUFFICIENT_SCOPE" || + code === "BYPASS_PREFIX_NOT_ALLOWED" + ) { + return code; + } + // Zod validation surface (T-011 emits BYPASS_PREFIX_NOT_ALLOWED inside + // `error.details[].message`). + const details = (errorField as { details?: unknown }).details; + if (Array.isArray(details)) { + for (const d of details) { + const m = (d as { message?: unknown }).message; + if (typeof m === "string" && m.includes("BYPASS_PREFIX_NOT_ALLOWED")) { + return "BYPASS_PREFIX_NOT_ALLOWED"; + } + } + } + } + return "GENERIC"; +} + +// ─── component ──────────────────────────────────────────────────────────── + +export default function AuthzSection() { + const t = useTranslations("settings"); + const [inventory, setInventory] = useState(null); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + + // Draft state — only persisted on Save (security-impacting fields require + // a password re-prompt before the PATCH is fired). + const [draftEnabled, setDraftEnabled] = useState(true); + const [draftPrefixes, setDraftPrefixes] = useState([]); + const [newPrefixInput, setNewPrefixInput] = useState(""); + + const [passwordModalOpen, setPasswordModalOpen] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [status, setStatus] = useState(null); + + const loadInventory = useCallback(async () => { + setLoading(true); + setLoadError(null); + try { + const res = await fetch("/api/settings/authz-inventory", { + credentials: "include", + }); + if (!res.ok) { + const code = parseErrorCode(await res.json().catch(() => null)); + setLoadError(t(`authz.error.${code}`)); + return; + } + const data = (await res.json()) as InventoryPayload; + setInventory(data); + setDraftEnabled(data.bypassEnabled); + setDraftPrefixes([...data.bypassPrefixes]); + } catch { + setLoadError(t("authz.loadError")); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + void loadInventory(); + }, [loadInventory]); + + const dirty = useMemo(() => { + if (!inventory) return false; + if (draftEnabled !== inventory.bypassEnabled) return true; + if (draftPrefixes.length !== inventory.bypassPrefixes.length) return true; + const a = [...draftPrefixes].sort(); + const b = [...inventory.bypassPrefixes].sort(); + return a.some((p, i) => p !== b[i]); + }, [draftEnabled, draftPrefixes, inventory]); + + const spawnCapable = inventory?.spawnCapablePrefixes ?? []; + + const isSpawnCapable = useCallback( + (prefix: string) => spawnCapable.some((p) => prefix === p || prefix.startsWith(p)), + [spawnCapable] + ); + + const handleAddPrefix = () => { + const trimmed = newPrefixInput.trim(); + if (!trimmed) return; + if (draftPrefixes.includes(trimmed)) { + setNewPrefixInput(""); + return; + } + setDraftPrefixes((prev) => [...prev, trimmed]); + setNewPrefixInput(""); + }; + + const handleRemovePrefix = (prefix: string) => { + if (isSpawnCapable(prefix)) return; + setDraftPrefixes((prev) => prev.filter((p) => p !== prefix)); + }; + + const handleSaveRequest = () => { + if (!dirty) return; + setCurrentPassword(""); + setStatus(null); + setPasswordModalOpen(true); + }; + + const handleSubmit = async () => { + if (!currentPassword) { + setStatus({ type: "error", message: t("authz.error.PASSWORD_REQUIRED") }); + return; + } + setSubmitting(true); + setStatus(null); + try { + const res = await fetch("/api/settings", { + method: "PATCH", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + localOnlyManageScopeBypassEnabled: draftEnabled, + localOnlyManageScopeBypassPrefixes: draftPrefixes, + currentPassword, + }), + }); + if (!res.ok) { + const payload = await res.json().catch(() => null); + const code = parseErrorCode(payload); + setStatus({ type: "error", message: t(`authz.error.${code}`) }); + return; + } + setStatus({ type: "success", message: t("authz.saved") }); + setPasswordModalOpen(false); + setCurrentPassword(""); + await loadInventory(); + } catch { + setStatus({ type: "error", message: t("authz.error.GENERIC") }); + } finally { + setSubmitting(false); + } + }; + + // ─── render ───────────────────────────────────────────────────────────── + + if (loading) { + return ( + +
+
+ +
+

{t("authz.title")}

+
+

{t("authz.loading")}

+
+ ); + } + + if (loadError || !inventory) { + return ( + +
+
+ +
+

{t("authz.title")}

+
+

{loadError ?? t("authz.loadError")}

+
+ ); + } + + return ( + <> + {/* Authz header + tier inventory */} + +
+
+ +
+
+

{t("authz.title")}

+

{t("authz.description")}

+
+
+ +
+ {inventory.tiers.map((tier) => ( +
+
+
+

{t(`authz.tier.${tier.name}`)}

+ {tier.bypassable && ( + + {t("authz.badge.bypassable")} + + )} +
+
+

{tier.description}

+
    + {tier.prefixes.map((prefix) => { + const badge = tierBadgeVariant( + tier.name, + prefix, + inventory.spawnCapablePrefixes, + inventory.bypassPrefixes, + inventory.bypassEnabled + ); + return ( +
  • + {prefix} + + {t(`authz.badge.${badge.key}`)} + +
  • + ); + })} +
+
+ ))} +
+
+ + {/* Bypass policy editor */} + +
+
+ +
+

{t("authz.bypass.section")}

+
+ +
+
+

{t("authz.bypass.kill_switch.label")}

+

{t("authz.bypass.kill_switch.desc")}

+
+ setDraftEnabled((prev) => !prev)} + disabled={submitting} + /> +
+ +
+
+

{t("authz.bypass.prefix.label")}

+

{t("authz.bypass.prefix.desc")}

+
+ + {draftPrefixes.length === 0 && ( +

{t("authz.bypass.prefix.empty")}

+ )} + +
    + {draftPrefixes.map((prefix) => { + const locked = isSpawnCapable(prefix); + return ( +
  • +
    + {prefix} + {locked && ( + + {t("authz.bypass.cli_tools_runtime_note")} + + )} +
    + +
  • + ); + })} + {/* Static read-only rows for spawn-capable prefixes that are NOT + in the draft list — surface them so the operator understands + they are intentionally not toggleable. */} + {spawnCapable + .filter((p) => !draftPrefixes.includes(p)) + .map((prefix) => ( +
  • +
    + {prefix} + + {t("authz.bypass.cli_tools_runtime_note")} + +
    + + {t("authz.badge.spawn_capable")} + +
  • + ))} +
+ +
+ setNewPrefixInput(e.target.value)} + disabled={submitting} + /> + +
+
+ + {/* Save bar */} +
+
+ {dirty && ( + {t("authz.pending")} + )} + {status && ( + + {status.message} + + )} +
+ +
+
+ + {/* Password re-auth modal — fires for every security-impacting PATCH */} + { + if (!submitting) { + setPasswordModalOpen(false); + setCurrentPassword(""); + } + }} + title={t("authz.password.prompt.label")} + footer={ +
+ + +
+ } + > +
+

{t("authz.password.prompt.desc")}

+ setCurrentPassword(e.target.value)} + autoFocus + /> + {status?.type === "error" &&

{status.message}

} +
+
+ + ); +} diff --git a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx index ff5e85de83..de5351852f 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx @@ -5,6 +5,7 @@ import { Card, Button, Input, Toggle } from "@/shared/components"; import { AI_PROVIDERS } from "@/shared/constants/providers"; import IPFilterSection from "./IPFilterSection"; import SessionInfoCard from "./SessionInfoCard"; +import AuthzSection from "./AuthzSection"; import { useTranslations } from "next-intl"; export default function SecurityTab() { @@ -274,6 +275,7 @@ export default function SecurityTab() { +
); } diff --git a/src/app/api/settings/authz-inventory/route.ts b/src/app/api/settings/authz-inventory/route.ts new file mode 100644 index 0000000000..a9bc671f5f --- /dev/null +++ b/src/app/api/settings/authz-inventory/route.ts @@ -0,0 +1,156 @@ +import { NextResponse } from "next/server"; +import { getSettings } from "@/lib/localDb"; +import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; +import { createErrorResponse } from "@/lib/api/errorResponse"; +import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; +import { + LOCAL_ONLY_API_PREFIXES, + ALWAYS_PROTECTED_API_PATHS, + LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES, + SPAWN_CAPABLE_PREFIXES, +} from "@/server/authz/routeGuard"; + +/** + * Static MANAGEMENT-tier example prefixes. Render-only — never consulted by + * the runtime policy. The actual MANAGEMENT classification is "any /api/* + * that is not LOCAL_ONLY, not v1/client, not on the public allowlist", so the + * inventory shows representative entries rather than a generated enumeration. + */ +const MANAGEMENT_TIER_PREFIXES: ReadonlyArray = [ + "/api/settings", + "/api/providers/", + "/api/api-keys", +]; + +const CLIENT_API_TIER_PREFIXES: ReadonlyArray = ["/v1/", "/api/v1/"]; + +const PUBLIC_TIER_PREFIXES: ReadonlyArray = ["/api/health", "/api/version", "/_next/"]; + +type TierName = "LOCAL_ONLY" | "ALWAYS_PROTECTED" | "MANAGEMENT" | "CLIENT_API" | "PUBLIC"; + +interface TierEntry { + name: TierName; + prefixes: string[]; + description: string; + bypassable: boolean; +} + +/** + * OQ-5: viewing the inventory requires authentication (dashboard session OR + * any valid API key, regardless of scope). The inventory leaks route-prefix + * taxonomy + current bypass state (reconnaissance value), so we never expose + * it anonymously — but a non-manage key holder may still inspect it. + * + * Compare with `requireManagementAuth` which would refuse anything below the + * manage scope; this endpoint is intentionally read-only and one rung lower. + */ +async function requireInventoryReadAuth(request: Request): Promise { + if (!(await isAuthRequired(request))) { + return null; + } + + if (await isDashboardSessionAuthenticated(request)) { + return null; + } + + const apiKey = extractApiKey(request); + if (apiKey) { + try { + if (await isValidApiKey(apiKey)) { + return null; + } + } catch { + return createErrorResponse({ + status: 503, + message: "Service temporarily unavailable", + type: "server_error", + }); + } + return createErrorResponse({ + status: 403, + message: "Invalid API key", + type: "invalid_request", + }); + } + + return createErrorResponse({ + status: 401, + message: "Authentication required", + type: "invalid_request", + }); +} + +function isBypassableConstant(prefix: string): boolean { + // A LOCAL_ONLY prefix is bypassable iff it appears in the compile-time + // bypass constant AND is not a SPAWN_CAPABLE prefix. Runtime DB state is + // surfaced separately via `bypassEnabled` / `bypassPrefixes`. + if (SPAWN_CAPABLE_PREFIXES.some((p) => p === prefix || prefix.startsWith(p))) { + return false; + } + return LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES.some((p) => p === prefix); +} + +export async function GET(request: Request) { + const authError = await requireInventoryReadAuth(request); + if (authError) return authError; + + try { + const settings = await getSettings(); + + const tiers: TierEntry[] = [ + { + name: "LOCAL_ONLY", + prefixes: [...LOCAL_ONLY_API_PREFIXES], + description: + "Loopback-only routes. Spawn child processes; exposing them to non-local traffic is a known CVE class (GHSA-fhh6-4qxv-rpqj). Some entries are opt-in bypassable via the manage scope (kill-switch gated).", + bypassable: LOCAL_ONLY_API_PREFIXES.some(isBypassableConstant), + }, + { + name: "ALWAYS_PROTECTED", + prefixes: [...ALWAYS_PROTECTED_API_PATHS], + description: + "Auth required unconditionally, even when requireLogin=false. Covers destructive / irreversible operations (shutdown, database settings).", + bypassable: false, + }, + { + name: "MANAGEMENT", + prefixes: [...MANAGEMENT_TIER_PREFIXES], + description: + "Default tier for /api/* admin endpoints. Auth required unless requireLogin=false. PATCHes touching security-impacting keys require currentPassword re-auth.", + bypassable: false, + }, + { + name: "CLIENT_API", + prefixes: [...CLIENT_API_TIER_PREFIXES], + description: + "Client-facing inference APIs. Accept Bearer API keys; not gated by dashboard sessions.", + bypassable: false, + }, + { + name: "PUBLIC", + prefixes: [...PUBLIC_TIER_PREFIXES], + description: "Unauthenticated routes: health probes, public assets, onboarding bootstrap.", + bypassable: false, + }, + ]; + + const bypassEnabled = + typeof settings.localOnlyManageScopeBypassEnabled === "boolean" + ? settings.localOnlyManageScopeBypassEnabled + : true; + const bypassPrefixesRaw = settings.localOnlyManageScopeBypassPrefixes; + const bypassPrefixes = Array.isArray(bypassPrefixesRaw) + ? bypassPrefixesRaw.filter((p): p is string => typeof p === "string") + : [...LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES]; + + return NextResponse.json({ + tiers, + bypassEnabled, + bypassPrefixes, + spawnCapablePrefixes: [...SPAWN_CAPABLE_PREFIXES], + }); + } catch (error) { + console.log("Error loading authz inventory:", error); + return NextResponse.json({ error: "Failed to load authz inventory" }, { status: 500 }); + } +} diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts index 403a2a6ff2..60da92f71c 100644 --- a/src/app/api/settings/route.ts +++ b/src/app/api/settings/route.ts @@ -13,6 +13,124 @@ import { verifyManagementPassword, } from "@/lib/auth/managementPassword"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance"; +import { isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; +import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth"; +import { extractApiKey } from "@/sse/services/auth"; +import { getApiKeyMetadata } from "@/lib/db/apiKeys"; + +/** + * Settings keys whose change broadens attack surface. Spec §Security: + * password re-auth is required when any of these is present in a PATCH body. + * + * - `localOnlyManageScopeBypassEnabled` / `localOnlyManageScopeBypassPrefixes`: + * T-011 bypass kill-switch + per-prefix list. Operator must re-confirm + * before broadening the LOCAL_ONLY carve-out. + * - `requireLogin`: dashboard login enforcement toggle. + * - `newPassword`: password rotation (existing). Handled by the same gate so + * the password-verify only fires ONCE per PATCH. + * + * Note: `mcpEnabled` is NOT gated server-side — the dedicated MCP page + * (/dashboard/mcp) toggles it via patchSetting() without a currentPassword + * prompt. The Authz section can still prompt client-side for consistency, + * but the server accepts the change without re-auth. + */ +const SECURITY_IMPACTING_KEYS = [ + "localOnlyManageScopeBypassEnabled", + "localOnlyManageScopeBypassPrefixes", + "requireLogin", + "newPassword", +] as const; + +/** + * Derive an audit actor string from the inbound request. Falls back to + * `"dashboard"` for cookie sessions, `"apikey:"` for Bearer API keys, + * `"cli"` for CLI machine-token sessions, and `"anonymous"` otherwise. Best + * effort — any lookup error degrades to `"unknown"` so the audit row still + * carries actor context. + */ +async function deriveAuditActor(request: Request): Promise { + try { + if (await isDashboardSessionAuthenticated(request)) return "dashboard"; + } catch { + /* fall through */ + } + try { + if (await isCliTokenAuthValid(request)) return "cli"; + } catch { + /* fall through */ + } + try { + const apiKey = extractApiKey(request); + if (apiKey) { + const meta = await getApiKeyMetadata(apiKey); + if (meta?.id) return `apikey:${meta.id}`; + return "apikey:unknown"; + } + } catch { + return "unknown"; + } + return "anonymous"; +} + +/** Deep-equality for diff detection. JSON round-trip handles plain settings. */ +function isDeepEqual(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (a === null || b === null) return false; + if (typeof a !== "object" || typeof b !== "object") return false; + try { + return JSON.stringify(a) === JSON.stringify(b); + } catch { + return false; + } +} + +/** Build per-key `{before, after}` diff for changed keys (top-level only). */ +function computeSettingsDiff( + before: Record, + after: Record, + candidateKeys: string[] +): Record { + const diff: Record = {}; + for (const key of candidateKeys) { + if (!isDeepEqual(before[key], after[key])) { + diff[key] = { before: before[key], after: after[key] }; + } + } + return diff; +} + +/** List of top-level body keys the operator attempted to change (audit context). */ +function attemptedKeysOf(body: Record | null | undefined): string[] { + if (!body || typeof body !== "object") return []; + return Object.keys(body).filter( + (k) => k !== "currentPassword" && k !== "newPassword" && k !== "password" + ); +} + +/** Emit a settings.update_failed row. Never throws — audit must not break flow. */ +function emitSettingsFailureAudit( + request: Request, + actor: string, + reason: string, + attemptedKeys: string[] +) { + try { + const { ipAddress, requestId } = getAuditRequestContext(request); + logAuditEvent({ + action: "settings.update_failed", + actor, + target: "settings", + resourceType: "settings", + status: "failure", + ipAddress: ipAddress || undefined, + requestId: requestId || undefined, + details: { reason, attempted_keys: attemptedKeys }, + }); + } catch { + /* best effort */ + } +} export async function GET(request: Request) { const authError = await requireManagementAuth(request); @@ -46,40 +164,105 @@ export async function PATCH(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; + // Derive actor + raw body once so the rejection paths can audit consistently. + const actor = await deriveAuditActor(request); + let rawBody: Record = {}; try { - const rawBody = await request.json(); + rawBody = (await request.json()) as Record; + } catch { + // Malformed JSON — surface a zod-style failure path so the rejection + // is auditable like every other 400. + emitSettingsFailureAudit(request, actor, "INVALID_JSON", []); + return NextResponse.json( + { error: { code: "INVALID_JSON", message: "Request body is not valid JSON" } }, + { status: 400 } + ); + } + const attemptedKeys = attemptedKeysOf(rawBody); + try { // Zod validation const validation = validateBody(updateSettingsSchema, rawBody); if (isValidationFailure(validation)) { + // Detect spawn-capable prefix rejection (spec AC-8) so the audit row + // names the correct error code; otherwise fall back to the generic + // validation-failure label. + const isBypassPrefixRejection = (validation.error.details || []).some( + (d) => typeof d.message === "string" && d.message.includes("BYPASS_PREFIX_NOT_ALLOWED") + ); + emitSettingsFailureAudit( + request, + actor, + isBypassPrefixRejection ? "BYPASS_PREFIX_NOT_ALLOWED" : "VALIDATION_FAILED", + attemptedKeys + ); return NextResponse.json({ error: validation.error }, { status: 400 }); } const body: typeof validation.data & { password?: string } = { ...validation.data }; - // If updating password, hash it - if (body.newPassword) { + // Security-impacting gate (T-011, spec AC-4 / AC-5). Computed from the + // VALIDATED body so we never trip on stray unknown keys. If any security + // key is present, require currentPassword + verify against the stored + // bcrypt hash. Dedupes with the previous inline newPassword reauth — the + // password is verified at most once per PATCH. + const touchedSecurityKeys = SECURITY_IMPACTING_KEYS.filter((k) => k in validation.data); + let storedPasswordHash = ""; + if (touchedSecurityKeys.length > 0) { const settings = await getSettings(); + // Lazy-hash any plaintext INITIAL_PASSWORD migration BEFORE we read the + // stored hash, so the gate works on fresh deploys too. const passwordState = await ensurePersistentManagementPasswordHash({ settings, - source: "settings.password_change", + source: "settings.security_impacting_update", }); - const currentHash = getStoredManagementPassword(passwordState.settings); - - if (currentHash) { + storedPasswordHash = getStoredManagementPassword(passwordState.settings); + // Cold-boot exception: same condition the existing newPassword path + // honoured before T-011 — when no password is configured yet AND login + // is currently disabled, allow the first write to set policy (incl. + // the password itself). Once a hash exists the gate always fires. + const isColdBoot = !storedPasswordHash && passwordState.settings.requireLogin === false; + if (!isColdBoot) { if (!body.currentPassword) { - return NextResponse.json({ error: "Current password required" }, { status: 400 }); + emitSettingsFailureAudit(request, actor, "PASSWORD_REQUIRED", attemptedKeys); + return NextResponse.json( + { + error: { + code: "PASSWORD_REQUIRED", + message: "currentPassword required for security-impacting setting changes", + keys: touchedSecurityKeys, + }, + }, + { status: 400 } + ); } - const isValid = await verifyManagementPassword(body.currentPassword, currentHash); + const isValid = await verifyManagementPassword(body.currentPassword, storedPasswordHash); if (!isValid) { - return NextResponse.json({ error: "Invalid current password" }, { status: 401 }); + emitSettingsFailureAudit(request, actor, "PASSWORD_MISMATCH", attemptedKeys); + return NextResponse.json( + { + error: { + code: "PASSWORD_MISMATCH", + message: "Invalid current password", + }, + }, + { status: 401 } + ); } } - - body.password = await hashManagementPassword(body.newPassword); - delete body.newPassword; - delete body.currentPassword; } + // Password rotation: hash the new value AFTER the gate has accepted the + // currentPassword (or the cold-boot exception fired). The gate already + // included `newPassword` in SECURITY_IMPACTING_KEYS, so no separate + // verify happens here — strictly hashing + body rewriting. + if (body.newPassword) { + body.password = await hashManagementPassword(body.newPassword); + delete body.newPassword; + } + delete body.currentPassword; + + // Snapshot BEFORE the write so the success row can record a real diff. + const beforeSnapshot = (await getSettings()) as Record; const settings = await updateSettings(body); // Sync CLIProxyAPI settings to upstream_proxy_config table @@ -88,6 +271,7 @@ export async function PATCH(request: Request) { if (cpaUrl && typeof cpaUrl === "string") { const urlValidation = validateProxyUrl(cpaUrl); if (urlValidation.valid === false) { + emitSettingsFailureAudit(request, actor, "CLIPROXY_URL_INVALID", attemptedKeys); return NextResponse.json( { error: `Invalid CLIProxyAPI URL: ${urlValidation.error}` }, { status: 400 } @@ -106,6 +290,29 @@ export async function PATCH(request: Request) { }); } + // Audit success — diff of changed keys only. Idempotent PATCH (no diff) + // intentionally writes NO row (spec §Observability + AC-9/AC-11). + try { + const afterSnapshot = settings as Record; + const candidateKeys = Object.keys(body); + const diff = computeSettingsDiff(beforeSnapshot, afterSnapshot, candidateKeys); + if (Object.keys(diff).length > 0) { + const { ipAddress, requestId } = getAuditRequestContext(request); + logAuditEvent({ + action: "settings.update", + actor, + target: "settings", + resourceType: "settings", + status: "success", + ipAddress: ipAddress || undefined, + requestId: requestId || undefined, + details: { diff }, + }); + } + } catch { + // Audit failure must never break the write — swallow. + } + const { password, ...safeSettings } = settings; return NextResponse.json(safeSettings); } catch (error) { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index e355954350..89bc969435 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -842,6 +842,7 @@ "settingsAi": "AI Settings", "settingsSecurity": "Security", "settingsFeatureFlags": "Feature Flags", + "settingsAuthz": "Authz", "settingsRouting": "Routing", "settingsResilience": "Resilience", "settingsAdvanced": "Advanced", @@ -913,6 +914,7 @@ "settingsAdvancedSubtitle": "Power user options", "settingsSecuritySubtitle": "Auth and encryption", "settingsFeatureFlagsSubtitle": "Toggle system capabilities", + "settingsAuthzSubtitle": "Route inventory and bypass policy", "docsSubtitle": "Documentation", "issuesSubtitle": "Report a bug", "changelogSubtitle": "Release notes" @@ -1288,6 +1290,7 @@ "keyName": "Key Name", "keyNamePlaceholder": "e.g. Production Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", + "managementAccessDesc": "Allow this API key to manage OmniRoute configuration.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", @@ -4587,7 +4590,62 @@ "claudeFastModeHint": "Anthropic does not officially support Fast Mode for SDK-style clients. When enabled, OmniRoute forwards an X-CPA-Force-Fast-Mode header so a paired CLIProxyAPI build can opt-in spoof the entrypoint. Only the listed Opus models are gated by Anthropic's client-side check. Subscription tier, Max plan, and Fast Mode credit balance are still enforced server-side — Anthropic may return out_of_credits even when the toggle is on.", "claudeFastModeModelsLabel": "Applied to models ({count})", "claudeFastModeModelCheckbox": "Enable Fast Mode for {model}", - "claudeFastModeSaveError": "Failed to update Claude Fast Mode setting" + "claudeFastModeSaveError": "Failed to update Claude Fast Mode setting", + "authz": { + "title": "Authz Inventory", + "description": "5-tier route classification with live bypass policy. Read shows the full taxonomy; mutations re-prompt for the management password.", + "loading": "Loading inventory…", + "loadError": "Failed to load authz inventory", + "tier": { + "LOCAL_ONLY": "Local only", + "ALWAYS_PROTECTED": "Always protected", + "MANAGEMENT": "Management", + "CLIENT_API": "Client API", + "PUBLIC": "Public" + }, + "bypass": { + "section": "Manage-scope bypass", + "kill_switch": { + "label": "Bypass kill-switch", + "desc": "Master toggle. When off, no LOCAL_ONLY prefix is reachable from non-loopback regardless of the per-prefix list." + }, + "prefix": { + "label": "Bypassable prefixes", + "desc": "LOCAL_ONLY prefixes that manage-scope API keys (or dashboard sessions) may reach from non-loopback hosts.", + "add": "Add prefix", + "placeholder": "/api/mcp/v2/", + "empty": "No prefixes configured. Bypass effectively off." + }, + "cli_tools_runtime_note": "Spawn-capable prefix. Compile-time deny; cannot be made bypassable. Shown read-only." + }, + "password": { + "prompt": { + "label": "Current password", + "desc": "Re-confirm to apply security-impacting changes." + }, + "placeholder": "Current management password", + "cancel": "Cancel", + "submit": "Apply" + }, + "save": "Save changes", + "saved": "Authz settings updated", + "pending": "Unsaved changes", + "badge": { + "bypassable": "Bypassable via manage scope", + "strict": "Strict loopback", + "auth_required": "Auth required", + "public": "Public", + "always_protected": "Always protected", + "spawn_capable": "Spawn-capable" + }, + "error": { + "PASSWORD_REQUIRED": "Current password required to apply these changes.", + "PASSWORD_MISMATCH": "Current password is incorrect.", + "INSUFFICIENT_SCOPE": "API key lacks the manage scope.", + "BYPASS_PREFIX_NOT_ALLOWED": "One or more prefixes target spawn-capable routes and cannot be bypassed.", + "GENERIC": "Failed to update authz settings." + } + } }, "contextRtk": { "title": "RTK Engine", diff --git a/src/lib/compliance/index.ts b/src/lib/compliance/index.ts index 560b260508..b966813c81 100644 --- a/src/lib/compliance/index.ts +++ b/src/lib/compliance/index.ts @@ -57,6 +57,11 @@ type AuditLogFilter = { }; type AuditLogRow = Record & { + id?: number | null; + action?: string | null; + actor?: string | null; + target?: string | null; + status?: string | null; details?: string | null; metadata?: string | null; ip_address?: string | null; @@ -65,6 +70,33 @@ type AuditLogRow = Record & { timestamp?: string | null; }; +/** + * Public shape of a normalized audit-log entry returned by `getAuditLog` / + * `normalizeAuditLogRow`. Includes the column-level fields callers (and + * tests) reach into directly — `action`, `actor`, `target`, `id`, `status`, + * `timestamp` — alongside the derived/parsed fields. The + * `Record` intersection preserves the existing behaviour of + * spreading any extra DB columns (e.g. schema additions) without losing + * compile-time access to the known ones. + */ +export type AuditLogEntry = Record & { + id?: number | null; + action?: string | null; + actor?: string | null; + target?: string | null; + status: string | null; + timestamp: string; + createdAt: string; + details: unknown; + metadata: unknown; + ip_address: string | null; + ip: string | null; + resource_type: string | null; + resourceType: string | null; + request_id: string | null; + requestId: string | null; +}; + const AUDIT_LOG_REQUIRED_COLUMNS: Record = { resource_type: "TEXT", status: "TEXT", @@ -227,7 +259,7 @@ function buildAuditLogQuery(filter: AuditLogFilter = {}): AuditLogQuery { }; } -function normalizeAuditLogRow(row: AuditLogRow) { +function normalizeAuditLogRow(row: AuditLogRow): AuditLogEntry { const details = parseAuditValue(row.details); const metadata = parseAuditValue(row.metadata); const resourceType = typeof row.resource_type === "string" ? row.resource_type : null; @@ -349,7 +381,7 @@ export function logAuditEvent(entry: { * @param {number} [filter.offset=0] - Pagination offset * @returns {Array<{ id: number, timestamp: string, action: string, actor: string, target: string, details: any, ip_address: string }>} */ -export function getAuditLog(filter: AuditLogFilter = {}) { +export function getAuditLog(filter: AuditLogFilter = {}): AuditLogEntry[] { const db = getDb(); if (!db) return []; diff --git a/src/lib/config/runtimeSettings.ts b/src/lib/config/runtimeSettings.ts index 634fd5d9b0..b22f48c294 100644 --- a/src/lib/config/runtimeSettings.ts +++ b/src/lib/config/runtimeSettings.ts @@ -14,13 +14,19 @@ export type RuntimeReloadSection = | "modelsDevSync" | "corsOrigins" | "ccBridgeTransforms" - | "systemTransforms"; + | "systemTransforms" + | "authzBypass"; export interface RuntimeReloadChange { section: RuntimeReloadSection; source: string; } +interface AuthzBypassSnapshot { + enabled: boolean; + prefixes: string[]; +} + interface RuntimeSettingsSnapshot { payloadRules: unknown; modelAliases: Record; @@ -35,8 +41,17 @@ interface RuntimeSettingsSnapshot { corsOrigins: string; ccBridgeTransforms: unknown; systemTransforms: unknown; + authzBypass: AuthzBypassSnapshot; } +// Default bypass policy: kill-switch on, `/api/mcp/` bypassable. Mirrors the +// pre-T-011 compile-time constant so the route guard works identically before +// the first `applyRuntimeSettings` call (e.g. cold-boot requests). +const DEFAULT_AUTHZ_BYPASS_SNAPSHOT: AuthzBypassSnapshot = { + enabled: true, + prefixes: ["/api/mcp/"], +}; + const DEFAULT_RUNTIME_SETTINGS_SNAPSHOT: RuntimeSettingsSnapshot = { payloadRules: null, modelAliases: {}, @@ -51,10 +66,17 @@ const DEFAULT_RUNTIME_SETTINGS_SNAPSHOT: RuntimeSettingsSnapshot = { corsOrigins: "", ccBridgeTransforms: null, systemTransforms: null, + authzBypass: DEFAULT_AUTHZ_BYPASS_SNAPSHOT, }; let lastAppliedSnapshot: RuntimeSettingsSnapshot | null = null; +// Module-local mirror of the current bypass policy. Read by the route guard +// on every non-loopback hit to a LOCAL_ONLY path via `getAuthzBypassSnapshot`. +// Initialised to the default so cold-boot requests (before any +// `applyRuntimeSettings` call) behave identically to PR #2473. +let currentAuthzBypass: AuthzBypassSnapshot = DEFAULT_AUTHZ_BYPASS_SNAPSHOT; + function isTruthyEnvFlag(value: string | undefined): boolean { if (typeof value !== "string") return false; return new Set(["1", "true", "yes", "on"]).has(value.trim().toLowerCase()); @@ -165,6 +187,41 @@ function normalizePayloadRules(value: unknown): unknown { return parseStoredJson(value, "payloadRules"); } +function normalizeAuthzBypass(settings: Record): AuthzBypassSnapshot { + const enabled = + settings.localOnlyManageScopeBypassEnabled === false + ? false + : settings.localOnlyManageScopeBypassEnabled === true + ? true + : DEFAULT_AUTHZ_BYPASS_SNAPSHOT.enabled; + const rawPrefixes = settings.localOnlyManageScopeBypassPrefixes; + const prefixes = Array.isArray(rawPrefixes) + ? Array.from( + new Set( + rawPrefixes + .map((entry) => (typeof entry === "string" ? entry.trim() : "")) + .filter((entry) => entry.length > 0 && entry.startsWith("/")) + ) + ) + : [...DEFAULT_AUTHZ_BYPASS_SNAPSHOT.prefixes]; + return { enabled, prefixes }; +} + +/** + * O(1) accessor for the current LOCAL_ONLY manage-scope bypass policy. + * + * Consumed by the route-guard hot path (`isLocalOnlyBypassableByManageScope`). + * Returns the default snapshot (`{ enabled: true, prefixes: ["/api/mcp/"] }`) + * before the first `applyRuntimeSettings` call so cold-boot requests behave + * identically to PR #2473. Mutated only by `applyAuthzBypassSection`. + * + * Hot-reload latency: <50 ms (no I/O, no async, pure read of module-local + * state). Spec §Non-Functional Requirements / Performance. + */ +export function getAuthzBypassSnapshot(): AuthzBypassSnapshot { + return currentAuthzBypass; +} + export function buildRuntimeSettingsSnapshot( settings: Record ): RuntimeSettingsSnapshot { @@ -188,6 +245,7 @@ export function buildRuntimeSettingsSnapshot( corsOrigins: typeof settings.corsOrigins === "string" ? settings.corsOrigins : "", ccBridgeTransforms: parseStoredJson(settings.ccBridgeTransforms, "ccBridgeTransforms"), systemTransforms: parseStoredJson(settings.systemTransforms, "systemTransforms"), + authzBypass: normalizeAuthzBypass(settings), }; } @@ -283,6 +341,14 @@ async function applyCcBridgeTransformsSection(ccBridgeTransforms: unknown) { } } +/** + * Swap the in-process bypass policy. Synchronous, O(1), no I/O — the SLA + * (<50 ms hot-reload) is structurally satisfied by this shape. + */ +function applyAuthzBypassSection(snapshot: AuthzBypassSnapshot) { + currentAuthzBypass = { enabled: snapshot.enabled, prefixes: [...snapshot.prefixes] }; +} + async function applySystemTransformsSection(systemTransforms: unknown) { const { setSystemTransformsConfig, resetSystemTransformsConfig } = await import("@omniroute/open-sse/services/systemTransforms.ts"); @@ -447,6 +513,11 @@ export async function applyRuntimeSettings( markChanged("systemTransforms"); } + if (force || hasChanged(currentSnapshot.authzBypass, previousSnapshot.authzBypass)) { + applyAuthzBypassSection(currentSnapshot.authzBypass); + markChanged("authzBypass"); + } + lastAppliedSnapshot = currentSnapshot; return changes; } @@ -457,4 +528,5 @@ export function getLastAppliedRuntimeSettingsSnapshotForTests() { export function resetRuntimeSettingsStateForTests() { lastAppliedSnapshot = null; + currentAuthzBypass = DEFAULT_AUTHZ_BYPASS_SNAPSHOT; } diff --git a/src/lib/db/apiKeys.ts b/src/lib/db/apiKeys.ts index f056550f88..aca2127cb7 100644 --- a/src/lib/db/apiKeys.ts +++ b/src/lib/db/apiKeys.ts @@ -109,6 +109,7 @@ interface ApiKeyView extends JsonRecord { isActive: boolean; accessSchedule: AccessSchedule | null; rateLimits: RateLimitRule[] | null; + scopes: string[]; } // LRU cache for API key validation (valid keys only) @@ -379,6 +380,7 @@ export async function getApiKeys() { camelRow.accessSchedule = parseAccessSchedule(camelRow.accessSchedule); camelRow.rateLimits = parseRateLimits(camelRow.rateLimits); camelRow.isBanned = parseIsBanned(camelRow.isBanned); + camelRow.scopes = parseStringList((camelRow as JsonRecord).scopes); if (typeof camelRow.id === "string" && camelRow.id.length > 0) { setNoLog(camelRow.id, camelRow.noLog === true); } @@ -400,6 +402,7 @@ export async function getApiKeyById(id: string) { camelRow.accessSchedule = parseAccessSchedule(camelRow.accessSchedule); camelRow.rateLimits = parseRateLimits(camelRow.rateLimits); camelRow.isBanned = parseIsBanned(camelRow.isBanned); + camelRow.scopes = parseStringList((camelRow as JsonRecord).scopes); if (typeof camelRow.id === "string" && camelRow.id.length > 0) { setNoLog(camelRow.id, camelRow.noLog === true); } @@ -762,14 +765,60 @@ export async function updateApiKeyPermissions( } const scopesUpdate = (normalized as Record).scopes; + const nextScopes: string[] = Array.isArray(scopesUpdate) + ? (scopesUpdate as unknown[]).filter((s): s is string => typeof s === "string") + : []; + // Capture previous scopes BEFORE the UPDATE so we can compare for the audit + // event below. We only fetch when the caller is actually changing scopes — + // a privileged change ("manage" grants management API surface access) that + // must always leave an audit trail per OWASP A09 / SOC2 CC7.2. + // + // The previous-scopes SELECT and the row UPDATE are wrapped in a single + // transaction so a concurrent writer cannot slip in between and make the + // audit log lie about what changed. SQLite is single-writer in practice, + // but the transaction also gives us atomicity if the underlying driver + // ever swaps to a backend that allows multiple writers (sqljsAdapter / + // nodeSqliteAdapter fall-back per v3.8.1 db driver cascade). + let previousScopes: string[] = []; + let changedRows = 0; if (scopesUpdate !== undefined) { updates.push("scopes = @scopes"); - params.scopes = JSON.stringify(Array.isArray(scopesUpdate) ? scopesUpdate : []); + params.scopes = JSON.stringify(nextScopes); + + // SELECT-then-UPDATE wrapped in an explicit transaction so a concurrent + // writer can't slip between the read and the write and make the audit + // log lie about what changed. `exec("BEGIN"/"COMMIT")` works across all + // driver backends (better-sqlite3 / node:sqlite / sql.js) wired by the + // v3.8.1 db driver cascade — none of them expose `db.transaction()` via + // ApiKeysDbLike, which is intentionally minimal. + db.exec("BEGIN IMMEDIATE"); + try { + const prevRow = db + .prepare<{ scopes: string | null }>("SELECT scopes FROM api_keys WHERE id = ?") + .get(id); + previousScopes = parseStringList(prevRow?.scopes ?? null); + const upd = db + .prepare(`UPDATE api_keys SET ${updates.join(", ")} WHERE id = @id`) + .run(params); + changedRows = upd.changes ?? 0; + db.exec("COMMIT"); + } catch (err) { + // Guard the ROLLBACK: if it throws (e.g. transaction already ended + // due to an implicit commit, or backend in a bad state), the original + // error from the try block is the actionable one — don't shadow it. + try { + db.exec("ROLLBACK"); + } catch { + // swallow: original error is more important + } + throw err; + } + } else { + const upd = db.prepare(`UPDATE api_keys SET ${updates.join(", ")} WHERE id = @id`).run(params); + changedRows = upd.changes ?? 0; } - const result = db.prepare(`UPDATE api_keys SET ${updates.join(", ")} WHERE id = @id`).run(params); - - if (result.changes === 0) return false; + if (changedRows === 0) return false; const { logAuditEvent } = await import("@/lib/compliance"); @@ -787,6 +836,38 @@ export async function updateApiKeyPermissions( }); } + if (scopesUpdate !== undefined) { + // Compare prev vs next scope sets and emit a dedicated audit event when + // the privileged "manage" scope is granted or revoked. Other scope + // mutations also emit a generic "apiKey.scopes.update" so the audit log + // captures the full change history (action + details). + const hadManage = previousScopes.includes("manage"); + const hasManage = nextScopes.includes("manage"); + if (!hadManage && hasManage) { + logAuditEvent({ + action: "apiKey.scopes.grant", + target: id, + details: { scopes: nextScopes, previous: previousScopes }, + }); + } else if (hadManage && !hasManage) { + logAuditEvent({ + action: "apiKey.scopes.revoke", + target: id, + details: { scopes: nextScopes, previous: previousScopes }, + }); + } else if ( + previousScopes.length !== nextScopes.length || + previousScopes.some((s) => !nextScopes.includes(s)) || + nextScopes.some((s) => !previousScopes.includes(s)) + ) { + logAuditEvent({ + action: "apiKey.scopes.update", + target: id, + details: { scopes: nextScopes, previous: previousScopes }, + }); + } + } + if (normalized.noLog !== undefined) { setNoLog(id, normalized.noLog); } @@ -985,6 +1066,28 @@ export async function getApiKeyMetadata( // persistent env-var key support (persistent passthrough keys) (#1350) if (isConfiguredEnvApiKey(key)) { + // ─── Env-key management-scope bypass ────────────────────────────────── + // The deployment-time env key (`OMNIROUTE_API_KEY` / `ROUTER_API_KEY`) + // is granted the "manage" scope unconditionally. This is intentional: + // + // 1. The env key never exists in the SQLite `api_keys` table, so the + // DB-backed scopes column does not apply. We synthesize the + // metadata record here. + // 2. The operator who set the env var is presumed to be the deployment + // owner; rotating (or unsetting) the env var is the only way to + // rotate this privilege. There is no UI to change it. + // 3. Management API access via the env key still passes through + // `requireManagementAuth` → `hasManageScope`, so policy decisions + // remain centralised in `src/server/authz/*`. + // 4. Requests authenticated by the env key are tagged with + // `id: "env-key"` for downstream audit-log emitters, making it + // possible to distinguish env-key activity from user-created keys + // that happen to also hold "manage". + // + // DO NOT remove "manage" from this list — that would break the + // deployment-time bootstrap path that operators rely on for headless + // / CI / first-boot scenarios. If you need to disable env-key access, + // unset the env var instead. return { id: "env-key", name: "Environment Key", diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts index 9d3837c420..ece2968e3a 100644 --- a/src/lib/db/settings.ts +++ b/src/lib/db/settings.ts @@ -104,6 +104,14 @@ export async function getSettings() { wsAuth: false, maxBodySizeMb: requestBodyLimitMbFromEnv(process.env.MAX_BODY_SIZE_BYTES), debugMode: true, + // LOCAL_ONLY manage-scope bypass policy defaults (T-011 / spec §Data Model). + // Preserves PR #2473 behaviour on migration — the bypass starts ENABLED + // for `/api/mcp/` so existing manage-scope Bearer clients keep working. + // Operators flip the kill-switch to false (or drop the prefix) via the + // Settings UI; the change hot-reloads through `applyRuntimeSettings` → + // `applyAuthzBypassSection` → `getAuthzBypassSnapshot()`. + localOnlyManageScopeBypassEnabled: true, + localOnlyManageScopeBypassPrefixes: ["/api/mcp/"], }; for (const row of rows) { const record = toRecord(row); diff --git a/src/server/authz/policies/management.ts b/src/server/authz/policies/management.ts index e34ffe5161..cb3d89b7c3 100644 --- a/src/server/authz/policies/management.ts +++ b/src/server/authz/policies/management.ts @@ -8,7 +8,12 @@ import { extractApiKey, isValidApiKey } from "../../../sse/services/auth"; import { getApiKeyMetadata } from "../../../lib/db/apiKeys"; import { hasManageScope } from "../../../lib/api/requireManagementAuth"; import { CLI_TOKEN_HEADER } from "../headers"; -import { isAlwaysProtectedPath, isLocalOnlyPath, isLoopbackHost } from "../routeGuard"; +import { + isAlwaysProtectedPath, + isLocalOnlyBypassableByManageScope, + isLocalOnlyPath, + isLoopbackHost, +} from "../routeGuard"; const MODEL_SYNC_MANAGEMENT_PATH = /^\/api\/providers\/[^/]+\/(sync-models|models)$/; @@ -41,10 +46,72 @@ export const managementPolicy: RoutePolicy = { const path = ctx.classification.normalizedPath; // Tier 1: local-only gate — block spawn-capable routes from non-loopback. - if (isLocalOnlyPath(path)) { - if (!isLoopbackRequest(ctx.request.headers)) { - return reject(403, "LOCAL_ONLY", "This endpoint requires localhost access"); + // + // Carve-out: a small allow-list of LOCAL_ONLY paths (see + // LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES) is reachable from non-loopback + // when the caller presents EITHER (a) a valid API key with the `manage` + // scope, or (b) an authenticated dashboard session. This lets: + // - headless / remote MCP clients drive the management surface with a + // manage-scope Bearer key, and + // - the Dashboard UI itself (cookie session) render its MCP pages + // (/api/mcp/status, /api/mcp/tools) from a public hostname. + // + // The strict-loopback default still applies to everything else (notably + // the subprocess-spawning /api/cli-tools/runtime/* surface, which is NOT + // in the bypass list). + // + // Anonymous (no Bearer / invalid key / wrong scope / no session) requests + // still hit the same 403 LOCAL_ONLY they did before. + if (isLocalOnlyPath(path) && !isLoopbackRequest(ctx.request.headers)) { + if (isLocalOnlyBypassableByManageScope(path)) { + const apiKey = extractApiKey(ctx.request as unknown as Request); + if (apiKey) { + try { + if (await isValidApiKey(apiKey)) { + const meta = await getApiKeyMetadata(apiKey); + if (meta && hasManageScope(meta.scopes)) { + // Distinguish admin vs manage in the audit label so log review + // can tell which privilege actually granted the bypass. + const grantedBy = meta.scopes.includes("admin") ? "admin" : "manage"; + return allow({ + kind: "management_key", + id: meta.id, + label: `api-key-${grantedBy}-scope-local-only-bypass`, + }); + } + } + } catch (err) { + // Auth backend (DB / file store) failure: surface as 503 so the + // caller can retry. Anything else (TypeError / ReferenceError / + // programmer error) is logged so it's not silently swallowed — + // the policy still degrades closed (503) to avoid leaking the + // route, but we leave a breadcrumb for ops. + console.error("[managementPolicy] manage-scope bypass auth check failed", err); + return reject(503, "AUTH_BACKEND_UNAVAILABLE", "Service temporarily unavailable"); + } + } + // Dashboard session bypass: the Dashboard UI itself needs to render + // /api/mcp/status, /api/mcp/tools, etc. from a public hostname. Cookie + // auth is already proof of an authenticated admin — same trust level + // as a manage-scope Bearer for the surface in scope here. + try { + if (await isDashboardSessionAuthenticated(ctx.request)) { + return allow({ + kind: "dashboard_session", + id: "dashboard", + label: "dashboard-session-local-only-bypass", + }); + } + } catch (err) { + // Mirror the manage-scope branch above: degrade closed (503) rather + // than leaking the route through an unhandled 500, but log a + // breadcrumb for ops. Session-store DB failure / cookie parsing + // error / JWT decode throw all land here. + console.error("[managementPolicy] dashboard-session bypass auth check failed", err); + return reject(503, "AUTH_BACKEND_UNAVAILABLE", "Service temporarily unavailable"); + } } + return reject(403, "LOCAL_ONLY", "This endpoint requires localhost access"); } if (isInternalModelSyncRequest(ctx)) { diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index d746f74fa9..f82cec1263 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -5,6 +5,15 @@ * child processes; exposing them to non-local traffic is a known CVE class * (GHSA-fhh6-4qxv-rpqj). Blocked unconditionally regardless of auth state. * + * Carve-out: paths matching the live manage-scope bypass list (DB-stored, + * read via `getAuthzBypassSnapshot()`) MAY also be accessed from + * non-loopback if and only if the request carries an API key with the + * `manage` scope (or an authenticated dashboard session — see + * `policies/management.ts`). The bypass is opt-in per prefix and can be + * killed globally via the `localOnlyManageScopeBypassEnabled` setting. + * Unauthenticated requests to bypassable paths are still rejected with + * 403 LOCAL_ONLY. + * * Tier 2 — ALWAYS_PROTECTED: auth is always required, even when * requireLogin=false. Covers destructive / irreversible operations. * @@ -12,6 +21,8 @@ * requireLogin=false (existing behaviour). */ +import { getAuthzBypassSnapshot } from "@/lib/config/runtimeSettings"; + const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray = [ @@ -19,6 +30,34 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray = [ "/api/cli-tools/runtime/", ]; +/** + * Compile-time deny-list: route prefixes that can spawn arbitrary local + * subprocesses on behalf of the caller. These MUST NEVER appear in the + * manage-scope bypass list — regardless of DB state — because reaching them + * from non-loopback would re-introduce the GHSA-fhh6-4qxv-rpqj surface that + * the LOCAL_ONLY tier exists to close. + * + * Enforced at two layers: + * 1. zod schema (`settingsSchemas.ts`): rejects `PATCH /api/settings` with + * error code `BYPASS_PREFIX_NOT_ALLOWED` if any entry in + * `localOnlyManageScopeBypassPrefixes` falls inside this set. + * 2. runtime (`isLocalOnlyBypassableByManageScope` below): even if a + * malformed DB row somehow claims a spawn-capable path is bypassable, + * the policy still refuses to honour it. + */ +export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray = ["/api/cli-tools/runtime/"]; + +/** + * Compile-time default of the manage-scope bypass list. Kept as an exported + * constant so the Settings inventory page (and audit code) can render the + * "available bypassable prefixes" choices independent of current DB state. + * + * The RUNTIME decision in `isLocalOnlyBypassableByManageScope` does NOT + * consult this constant — it reads `getAuthzBypassSnapshot().prefixes`, + * which is hot-reloaded on every settings PATCH. + */ +export const LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES: ReadonlyArray = ["/api/mcp/"]; + export const ALWAYS_PROTECTED_API_PATHS: ReadonlyArray = [ "/api/shutdown", "/api/settings/database", @@ -42,6 +81,38 @@ export function isLocalOnlyPath(path: string): boolean { return LOCAL_ONLY_API_PREFIXES.some((p) => path === p || path.startsWith(p)); } +/** + * Runtime predicate consulted by the management policy on every non-loopback + * request to a LOCAL_ONLY path. Reads the live snapshot: + * - returns false if the global kill-switch is off + * (`localOnlyManageScopeBypassEnabled === false`), + * - returns true iff `path` matches one of the live bypass prefixes AND + * that prefix is not in `SPAWN_CAPABLE_PREFIXES` (defence-in-depth: the + * zod schema already rejects spawn-capable entries, but a malformed DB + * row should not be able to grant a bypass). + * + * O(1) (no I/O, no async). Hot-reload SLA: <50 ms — satisfied structurally. + */ +export function isLocalOnlyBypassableByManageScope(path: string): boolean { + const snapshot = getAuthzBypassSnapshot(); + if (!snapshot.enabled) return false; + return snapshot.prefixes.some((p) => { + // Defence-in-depth: reject a bypass prefix that is the same as, child of, + // OR PARENT of any spawn-capable prefix. The parent case catches e.g. + // `/api/cli-tools/` (parent of `/api/cli-tools/runtime/`) — a request to + // `/api/cli-tools/runtime/foo` would otherwise satisfy `path.startsWith(p)` + // and reach the spawn-capable surface without a loopback check. + if ( + SPAWN_CAPABLE_PREFIXES.some( + (spawn) => p === spawn || p.startsWith(spawn) || spawn.startsWith(p) + ) + ) { + return false; + } + return path === p || path.startsWith(p); + }); +} + export function isAlwaysProtectedPath(path: string): boolean { return ALWAYS_PROTECTED_API_PATHS.some((p) => path === p || path.startsWith(p)); } diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index 2e42dfd47f..8452cc1099 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -75,6 +75,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [ "settings-advanced", "settings-security", "settings-feature-flags", + "settings-authz", // Help "docs", "issues", @@ -659,6 +660,13 @@ const CONFIGURATION_ITEMS: readonly SidebarItemDefinition[] = [ subtitleKey: "settingsFeatureFlagsSubtitle", icon: "flag", }, + { + id: "settings-authz", + href: "/dashboard/settings/authz", + i18nKey: "settingsAuthz", + subtitleKey: "settingsAuthzSubtitle", + icon: "shield_lock", + }, ]; const HELP_ITEMS: readonly SidebarItemDefinition[] = [ diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index b93409a1ca..601717bb94 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -10,251 +10,282 @@ import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; import { ACCOUNT_FALLBACK_STRATEGY_VALUES } from "@/shared/constants/routingStrategies"; +import { SPAWN_CAPABLE_PREFIXES } from "@/server/authz/routeGuard"; const signatureCacheModeValues = ["enabled", "bypass", "bypass-strict"] as const; -export const updateSettingsSchema = z.object({ - newPassword: z.string().min(1).max(200).optional(), - currentPassword: z.string().max(200).optional(), - theme: z.string().max(50).optional(), - language: z.string().max(10).optional(), - requireLogin: z.boolean().optional(), - enableSocks5Proxy: z.boolean().optional(), - instanceName: z.string().max(100).optional(), - customLogoUrl: z.string().max(2000).optional(), - customLogoBase64: z.string().max(100000).optional(), - customFaviconUrl: z.string().max(2000).optional(), - customFaviconBase64: z.string().max(50000).optional(), - corsOrigins: z.string().max(500).optional(), - cloudUrl: z.string().max(500).optional(), - baseUrl: z.string().max(500).optional(), - setupComplete: z.boolean().optional(), - blockedProviders: z.array(z.string().max(100)).optional(), - hideHealthCheckLogs: z.boolean().optional(), - hideEndpointCloudflaredTunnel: z.boolean().optional(), - hideEndpointTailscaleFunnel: z.boolean().optional(), - hideEndpointNgrokTunnel: z.boolean().optional(), - debugMode: z.boolean().optional(), - hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), - comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), - codexServiceTier: z - .object({ - enabled: z.boolean().optional(), - tier: z.enum(["default", "priority", "flex"]).optional(), - supportedModels: z.array(z.string().max(200)).max(200).optional(), - }) - .optional(), - // Claude Fast Mode: opt-in toggle that asks a paired CLIProxyAPI build - // (claude-fastmode-spoof) to rewrite SDK-shaped entrypoints so requests can - // reach Anthropic Fast Mode (speed:"fast"). Default off; only the listed - // Opus models are gated by the Anthropic binary KT() check. Schema is - // intentionally permissive on supportedModels so additional eligible model - // ids can be enabled without a schema bump. - claudeFastMode: z - .object({ - enabled: z.boolean().optional(), - supportedModels: z.array(z.string().max(200)).max(200).optional(), - }) - .optional(), - // Routing settings (#134) - fallbackStrategy: z.enum(ACCOUNT_FALLBACK_STRATEGY_VALUES).optional(), - wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(), - stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(), - requestRetry: z.number().int().min(0).max(10).optional(), - maxRetryIntervalSec: z.number().int().min(0).max(300).optional(), - maxBodySizeMb: z - .number() - .int() - .min(MIN_REQUEST_BODY_LIMIT_MB) - .max(MAX_REQUEST_BODY_LIMIT_MB) - .optional(), - // Auto intent classifier settings (multilingual routing) - intentDetectionEnabled: z.boolean().optional(), - intentSimpleMaxWords: z.number().int().min(1).max(500).optional(), - intentExtraCodeKeywords: z.array(z.string().max(100)).optional(), - intentExtraReasoningKeywords: z.array(z.string().max(100)).optional(), - intentExtraSimpleKeywords: z.array(z.string().max(100)).optional(), - // Protocol toggles (default: disabled) - mcpEnabled: z.boolean().optional(), - mcpTransport: z.enum(["stdio", "sse", "streamable-http"]).optional(), - a2aEnabled: z.boolean().optional(), - wsAuth: z.boolean().optional(), - // CLI Fingerprint compatibility (per-provider) - cliCompatProviders: z.array(z.string().max(100)).optional(), - // CC bridge transforms (issue #2260): config-driven pipeline that normalizes - // system blocks at the Claude Code bridge so any client (OpenCode, Cline, - // Cursor, Continue, raw API) ends up with classifier-correct structure. - ccBridgeTransforms: z - .object({ - enabled: z.boolean(), - pipeline: z - .array( - z.discriminatedUnion("kind", [ - z.object({ - kind: z.literal("drop_paragraph_if_contains"), - needles: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("drop_paragraph_if_starts_with"), - prefixes: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_text"), - match: z.string().min(1).max(500), - replacement: z.string().max(500), - allOccurrences: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_regex"), - pattern: z.string().min(1).max(500), - flags: z.string().max(10).optional(), - replacement: z.string().max(500), - }), - z.object({ - kind: z.literal("drop_block_if_contains"), - needles: z.array(z.string().max(500)).max(50), - }), - z.object({ - kind: z.literal("prepend_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("append_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("inject_billing_header"), - entrypoint: z.string().min(1).max(50), - versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), - cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), - version: z.string().max(50).optional(), - }), - ]) - ) - .max(50), - }) - .optional(), - // System Transforms (issue #2260 v2): generic per-provider DSL covering - // native `claude`, `anthropic-compatible-cc-*` bridge, and any other - // provider key. Adds `obfuscate_words` op kind on top of the base set. - systemTransforms: z - .object({ - providers: z.record( - z.string().max(100), - z.object({ - enabled: z.boolean(), - pipeline: z - .array( - z.discriminatedUnion("kind", [ - z.object({ - kind: z.literal("drop_paragraph_if_contains"), - needles: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("drop_paragraph_if_starts_with"), - prefixes: z.array(z.string().max(500)).max(50), - caseSensitive: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_text"), - match: z.string().min(1).max(500), - replacement: z.string().max(500), - allOccurrences: z.boolean().optional(), - }), - z.object({ - kind: z.literal("replace_regex"), - pattern: z.string().min(1).max(500), - flags: z.string().max(10).optional(), - replacement: z.string().max(500), - }), - z.object({ - kind: z.literal("drop_block_if_contains"), - needles: z.array(z.string().max(500)).max(50), - }), - z.object({ - kind: z.literal("prepend_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("append_system_block"), - text: z.string().min(1).max(2000), - idempotencyKey: z.string().max(100).optional(), - }), - z.object({ - kind: z.literal("inject_billing_header"), - entrypoint: z.string().min(1).max(50), - versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), - cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), - version: z.string().max(50).optional(), - }), - z.object({ - kind: z.literal("obfuscate_words"), - words: z.array(z.string().max(100)).max(200), - targets: z - .array(z.enum(["system", "messages", "tools"])) - .max(3) - .optional(), - }), - ]) - ) - .max(50), - }) - ), - }) - .optional(), - // Strip provider/model prefix at proxy layer (e.g. "openai/gpt-4" → "gpt-4") - stripModelPrefix: z.boolean().optional(), - // Cache control preservation mode - alwaysPreserveClientCache: z.enum(["auto", "always", "never"]).optional(), - antigravitySignatureCacheMode: z.enum(signatureCacheModeValues).optional(), - // Adaptive Volume Routing - adaptiveVolumeRouting: z.boolean().optional(), - // Usage token buffer — safety margin added to reported prompt/input token counts. - // Prevents CLI tools from overrunning context windows. Set to 0 to disable. - usageTokenBuffer: z.number().int().min(0).max(50000).optional(), - // Custom CLI agent definitions for ACP - customAgents: z - .array( - z.object({ - id: z.string().max(50), - name: z.string().max(100), - binary: z.string().max(200), - versionCommand: z.string().max(300), - providerAlias: z.string().max(50), - spawnArgs: z.array(z.string().max(200)), - protocol: z.enum(["stdio", "http"]), +export const updateSettingsSchema = z + .object({ + newPassword: z.string().min(1).max(200).optional(), + currentPassword: z.string().max(200).optional(), + theme: z.string().max(50).optional(), + language: z.string().max(10).optional(), + requireLogin: z.boolean().optional(), + enableSocks5Proxy: z.boolean().optional(), + instanceName: z.string().max(100).optional(), + customLogoUrl: z.string().max(2000).optional(), + customLogoBase64: z.string().max(100000).optional(), + customFaviconUrl: z.string().max(2000).optional(), + customFaviconBase64: z.string().max(50000).optional(), + corsOrigins: z.string().max(500).optional(), + cloudUrl: z.string().max(500).optional(), + baseUrl: z.string().max(500).optional(), + setupComplete: z.boolean().optional(), + blockedProviders: z.array(z.string().max(100)).optional(), + hideHealthCheckLogs: z.boolean().optional(), + hideEndpointCloudflaredTunnel: z.boolean().optional(), + hideEndpointTailscaleFunnel: z.boolean().optional(), + hideEndpointNgrokTunnel: z.boolean().optional(), + debugMode: z.boolean().optional(), + hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), + comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), + codexServiceTier: z + .object({ + enabled: z.boolean().optional(), + tier: z.enum(["default", "priority", "flex"]).optional(), + supportedModels: z.array(z.string().max(200)).max(200).optional(), }) - ) - .optional(), - // SkillsMP marketplace API key - skillsmpApiKey: z.string().max(200).optional(), - // Active skills provider (single source of truth for skills page) - skillsProvider: z.enum(["skillsmp", "skillssh"]).optional(), - // models.dev sync settings - modelsDevSyncEnabled: z.boolean().optional(), - modelsDevSyncInterval: z.number().int().min(3600000).max(604800000).optional(), - // Vision Bridge settings - visionBridgeEnabled: z.boolean().optional(), - visionBridgeModel: z.string().max(200).optional(), - visionBridgePrompt: z.string().max(5000).optional(), - visionBridgeTimeout: z.number().int().min(1000).max(300000).optional(), - visionBridgeMaxImages: z.number().int().min(1).max(20).optional(), - // Missing settings - lkgpEnabled: z.boolean().optional(), - backgroundDegradation: z.unknown().optional(), - bruteForceProtection: z.boolean().optional(), - // Auto-routing settings - autoRoutingEnabled: z.boolean().optional(), - autoRoutingDefaultVariant: z - .enum(["lkgp", "coding", "fast", "cheap", "offline", "smart"]) - .optional(), -}); + .optional(), + // Claude Fast Mode: opt-in toggle that asks a paired CLIProxyAPI build + // (claude-fastmode-spoof) to rewrite SDK-shaped entrypoints so requests can + // reach Anthropic Fast Mode (speed:"fast"). Default off; only the listed + // Opus models are gated by the Anthropic binary KT() check. Schema is + // intentionally permissive on supportedModels so additional eligible model + // ids can be enabled without a schema bump. + claudeFastMode: z + .object({ + enabled: z.boolean().optional(), + supportedModels: z.array(z.string().max(200)).max(200).optional(), + }) + .optional(), + // Routing settings (#134) + fallbackStrategy: z.enum(ACCOUNT_FALLBACK_STRATEGY_VALUES).optional(), + wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(), + stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(), + requestRetry: z.number().int().min(0).max(10).optional(), + maxRetryIntervalSec: z.number().int().min(0).max(300).optional(), + maxBodySizeMb: z + .number() + .int() + .min(MIN_REQUEST_BODY_LIMIT_MB) + .max(MAX_REQUEST_BODY_LIMIT_MB) + .optional(), + // Auto intent classifier settings (multilingual routing) + intentDetectionEnabled: z.boolean().optional(), + intentSimpleMaxWords: z.number().int().min(1).max(500).optional(), + intentExtraCodeKeywords: z.array(z.string().max(100)).optional(), + intentExtraReasoningKeywords: z.array(z.string().max(100)).optional(), + intentExtraSimpleKeywords: z.array(z.string().max(100)).optional(), + // Protocol toggles (default: disabled) + mcpEnabled: z.boolean().optional(), + mcpTransport: z.enum(["stdio", "sse", "streamable-http"]).optional(), + a2aEnabled: z.boolean().optional(), + wsAuth: z.boolean().optional(), + // CLI Fingerprint compatibility (per-provider) + cliCompatProviders: z.array(z.string().max(100)).optional(), + // CC bridge transforms (issue #2260): config-driven pipeline that normalizes + // system blocks at the Claude Code bridge so any client (OpenCode, Cline, + // Cursor, Continue, raw API) ends up with classifier-correct structure. + ccBridgeTransforms: z + .object({ + enabled: z.boolean(), + pipeline: z + .array( + z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("drop_paragraph_if_contains"), + needles: z.array(z.string().max(500)).max(50), + caseSensitive: z.boolean().optional(), + }), + z.object({ + kind: z.literal("drop_paragraph_if_starts_with"), + prefixes: z.array(z.string().max(500)).max(50), + caseSensitive: z.boolean().optional(), + }), + z.object({ + kind: z.literal("replace_text"), + match: z.string().min(1).max(500), + replacement: z.string().max(500), + allOccurrences: z.boolean().optional(), + }), + z.object({ + kind: z.literal("replace_regex"), + pattern: z.string().min(1).max(500), + flags: z.string().max(10).optional(), + replacement: z.string().max(500), + }), + z.object({ + kind: z.literal("drop_block_if_contains"), + needles: z.array(z.string().max(500)).max(50), + }), + z.object({ + kind: z.literal("prepend_system_block"), + text: z.string().min(1).max(2000), + idempotencyKey: z.string().max(100).optional(), + }), + z.object({ + kind: z.literal("append_system_block"), + text: z.string().min(1).max(2000), + idempotencyKey: z.string().max(100).optional(), + }), + z.object({ + kind: z.literal("inject_billing_header"), + entrypoint: z.string().min(1).max(50), + versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), + cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), + version: z.string().max(50).optional(), + }), + ]) + ) + .max(50), + }) + .optional(), + // System Transforms (issue #2260 v2): generic per-provider DSL covering + // native `claude`, `anthropic-compatible-cc-*` bridge, and any other + // provider key. Adds `obfuscate_words` op kind on top of the base set. + systemTransforms: z + .object({ + providers: z.record( + z.string().max(100), + z.object({ + enabled: z.boolean(), + pipeline: z + .array( + z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("drop_paragraph_if_contains"), + needles: z.array(z.string().max(500)).max(50), + caseSensitive: z.boolean().optional(), + }), + z.object({ + kind: z.literal("drop_paragraph_if_starts_with"), + prefixes: z.array(z.string().max(500)).max(50), + caseSensitive: z.boolean().optional(), + }), + z.object({ + kind: z.literal("replace_text"), + match: z.string().min(1).max(500), + replacement: z.string().max(500), + allOccurrences: z.boolean().optional(), + }), + z.object({ + kind: z.literal("replace_regex"), + pattern: z.string().min(1).max(500), + flags: z.string().max(10).optional(), + replacement: z.string().max(500), + }), + z.object({ + kind: z.literal("drop_block_if_contains"), + needles: z.array(z.string().max(500)).max(50), + }), + z.object({ + kind: z.literal("prepend_system_block"), + text: z.string().min(1).max(2000), + idempotencyKey: z.string().max(100).optional(), + }), + z.object({ + kind: z.literal("append_system_block"), + text: z.string().min(1).max(2000), + idempotencyKey: z.string().max(100).optional(), + }), + z.object({ + kind: z.literal("inject_billing_header"), + entrypoint: z.string().min(1).max(50), + versionFormat: z.enum(["ex-machina", "omniroute-daystamp"]), + cchAlgo: z.enum(["sha256-first-user", "xxhash64-body", "static-zero"]), + version: z.string().max(50).optional(), + }), + z.object({ + kind: z.literal("obfuscate_words"), + words: z.array(z.string().max(100)).max(200), + targets: z + .array(z.enum(["system", "messages", "tools"])) + .max(3) + .optional(), + }), + ]) + ) + .max(50), + }) + ), + }) + .optional(), + // Strip provider/model prefix at proxy layer (e.g. "openai/gpt-4" → "gpt-4") + stripModelPrefix: z.boolean().optional(), + // Cache control preservation mode + alwaysPreserveClientCache: z.enum(["auto", "always", "never"]).optional(), + antigravitySignatureCacheMode: z.enum(signatureCacheModeValues).optional(), + // Adaptive Volume Routing + adaptiveVolumeRouting: z.boolean().optional(), + // Usage token buffer — safety margin added to reported prompt/input token counts. + // Prevents CLI tools from overrunning context windows. Set to 0 to disable. + usageTokenBuffer: z.number().int().min(0).max(50000).optional(), + // Custom CLI agent definitions for ACP + customAgents: z + .array( + z.object({ + id: z.string().max(50), + name: z.string().max(100), + binary: z.string().max(200), + versionCommand: z.string().max(300), + providerAlias: z.string().max(50), + spawnArgs: z.array(z.string().max(200)), + protocol: z.enum(["stdio", "http"]), + }) + ) + .optional(), + // SkillsMP marketplace API key + skillsmpApiKey: z.string().max(200).optional(), + // Active skills provider (single source of truth for skills page) + skillsProvider: z.enum(["skillsmp", "skillssh"]).optional(), + // models.dev sync settings + modelsDevSyncEnabled: z.boolean().optional(), + modelsDevSyncInterval: z.number().int().min(3600000).max(604800000).optional(), + // Vision Bridge settings + visionBridgeEnabled: z.boolean().optional(), + visionBridgeModel: z.string().max(200).optional(), + visionBridgePrompt: z.string().max(5000).optional(), + visionBridgeTimeout: z.number().int().min(1000).max(300000).optional(), + visionBridgeMaxImages: z.number().int().min(1).max(20).optional(), + // Missing settings + lkgpEnabled: z.boolean().optional(), + backgroundDegradation: z.unknown().optional(), + bruteForceProtection: z.boolean().optional(), + // Auto-routing settings + autoRoutingEnabled: z.boolean().optional(), + autoRoutingDefaultVariant: z + .enum(["lkgp", "coding", "fast", "cheap", "offline", "smart"]) + .optional(), + // LOCAL_ONLY manage-scope bypass policy (T-011). Kill-switch + per-prefix + // list, both DB-stored and hot-reloaded into `getAuthzBypassSnapshot()` on + // each PATCH. The prefix list MUST NOT include any spawn-capable path — + // enforced here and at runtime (GHSA-fhh6-4qxv-rpqj rationale). + localOnlyManageScopeBypassEnabled: z.boolean().optional(), + localOnlyManageScopeBypassPrefixes: z.array(z.string().startsWith("/")).max(20).optional(), + }) + .superRefine((data, ctx) => { + const prefixes = data.localOnlyManageScopeBypassPrefixes; + if (!prefixes) return; + for (let i = 0; i < prefixes.length; i++) { + const prefix = prefixes[i]; + // Reject prefixes that are the same as, child of, or PARENT of any + // spawn-capable prefix. The parent case catches e.g. `/api/cli-tools/` + // — a bypass on the parent would grant non-loopback access to the + // spawn-capable `/api/cli-tools/runtime/*` surface via path.startsWith. + if ( + SPAWN_CAPABLE_PREFIXES.some( + (spawn) => prefix === spawn || prefix.startsWith(spawn) || spawn.startsWith(prefix) + ) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["localOnlyManageScopeBypassPrefixes", i], + message: `BYPASS_PREFIX_NOT_ALLOWED: ${prefix} is spawn-capable and cannot be bypassed`, + params: { code: "BYPASS_PREFIX_NOT_ALLOWED", prefix }, + }); + } + } + }); export const databaseSettingsSchema = z.object( { diff --git a/src/types/settings.ts b/src/types/settings.ts index 0fb9956f68..2559944868 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -26,6 +26,12 @@ export interface Settings { hideEndpointNgrokTunnel?: boolean; hiddenSidebarItems?: HideableSidebarItemId[]; resilienceSettings?: ResilienceSettings; + // LOCAL_ONLY manage-scope bypass policy (DB-stored, hot-reloaded by + // `applyRuntimeSettings` → `applyAuthzBypassSection`). The route guard + // consults `getAuthzBypassSnapshot()` on the hot path; these fields are + // the persisted source of truth that feeds that snapshot. + localOnlyManageScopeBypassEnabled?: boolean; + localOnlyManageScopeBypassPrefixes?: string[]; } export interface ComboDefaults { diff --git a/tests/unit/_mocks/settings.ts b/tests/unit/_mocks/settings.ts new file mode 100644 index 0000000000..535f28cdf6 --- /dev/null +++ b/tests/unit/_mocks/settings.ts @@ -0,0 +1,80 @@ +/** + * Shared settings test fixture. + * + * Backs onto a real isolated SQLite DB + the production + * `updateSettings → applyRuntimeSettings` pipeline so callers exercise the + * actual hot-reload path. Tests that need to mock `getAuthzBypassSnapshot` + * directly defeat the integration value of AC-7 — use this helper instead. + * + * Usage: + * ```ts + * import { setupSettingsFixture, mockSettings, resetSettingsMock } from "../_mocks/settings"; + * const fixture = setupSettingsFixture("authz-bypass"); + * test.beforeEach(() => fixture.resetStorage()); + * await mockSettings({ localOnlyManageScopeBypassEnabled: false }); + * ``` + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { Settings } from "../../../src/types/settings"; + +let activeFixture: SettingsFixture | null = null; + +export interface SettingsFixture { + testDataDir: string; + resetStorage(): Promise; + cleanup(): void; +} + +/** + * Allocate an isolated DATA_DIR + reset DB state per test. Must run BEFORE + * any DB modules are imported by the test file. + */ +export function setupSettingsFixture(slug: string): SettingsFixture { + const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), `omr-settings-mock-${slug}-`)); + process.env.DATA_DIR = testDataDir; + if (!process.env.API_KEY_SECRET) { + process.env.API_KEY_SECRET = `test-settings-mock-secret-${Date.now()}`; + } + + const fixture: SettingsFixture = { + testDataDir, + async resetStorage() { + const core = await import("../../../src/lib/db/core.ts"); + const runtime = await import("../../../src/lib/config/runtimeSettings.ts"); + core.resetDbInstance(); + runtime.resetRuntimeSettingsStateForTests(); + fs.rmSync(testDataDir, { recursive: true, force: true }); + fs.mkdirSync(testDataDir, { recursive: true }); + }, + cleanup() { + fs.rmSync(testDataDir, { recursive: true, force: true }); + }, + }; + activeFixture = fixture; + return fixture; +} + +/** + * Write a partial Settings patch through the production + * `updateSettings → applyRuntimeSettings` pipeline. Hot-reload side effects + * (route guard snapshot, etc.) fire exactly as they do in `PATCH /api/settings`. + */ +export async function mockSettings(partial: Partial): Promise> { + const settingsDb = await import("../../../src/lib/db/settings.ts"); + return settingsDb.updateSettings(partial as Record); +} + +/** + * Reset the in-process runtime snapshot to the cold-boot default. Called by + * test `beforeEach` hooks that need a clean slate without nuking the whole + * fixture directory. + */ +export async function resetSettingsMock(): Promise { + const runtime = await import("../../../src/lib/config/runtimeSettings.ts"); + runtime.resetRuntimeSettingsStateForTests(); + if (activeFixture) { + await activeFixture.resetStorage(); + } +} diff --git a/tests/unit/api/authz-inventory.test.ts b/tests/unit/api/authz-inventory.test.ts new file mode 100644 index 0000000000..e17d7bf797 --- /dev/null +++ b/tests/unit/api/authz-inventory.test.ts @@ -0,0 +1,210 @@ +/** + * T-013 — GET /api/settings/authz-inventory. + * + * Covers spec AC-1, AC-2, AC-12 (and AC-13 PATCH coverage continues to live + * in `tests/unit/settings/authz-bypass.test.ts`; this file only asserts the + * inventory endpoint itself). + * + * - AC-1 response shape: 5 tiers, each with prefixes; bypassEnabled / bypassPrefixes / spawnCapablePrefixes present. + * - AC-2 bypass-state flags match getSettings() and update after PATCH. + * - AC-12 anonymous request → 401/403 (no inventory leak). + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { setupSettingsFixture } from "../_mocks/settings.ts"; +import { makeManagementSessionRequest } from "../../helpers/managementSession.ts"; + +const fixture = setupSettingsFixture("authz-inventory"); +process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1"; + +const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; +const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; + +const core = await import("../../../src/lib/db/core.ts"); +const settingsDb = await import("../../../src/lib/db/settings.ts"); +const runtime = await import("../../../src/lib/config/runtimeSettings.ts"); +const inventoryRoute = await import("../../../src/app/api/settings/authz-inventory/route.ts"); +const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); + +test.beforeEach(async () => { + await fixture.resetStorage(); + apiKeysDb.resetApiKeyState(); + runtime.resetRuntimeSettingsStateForTests(); +}); + +test.after(() => { + core.resetDbInstance(); + fixture.cleanup(); + if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET; + if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD; + else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; +}); + +// ─── AC-1 — shape ───────────────────────────────────────────────────────── + +test("AC-1: GET returns 5 tiers with prefixes + bypass state envelope", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-ac1"; + await settingsDb.updateSettings({ requireLogin: true }); + + const request = await makeManagementSessionRequest( + "http://localhost/api/settings/authz-inventory", + { method: "GET" } + ); + const response = await inventoryRoute.GET(request); + assert.equal(response.status, 200); + + const body = (await response.json()) as { + tiers: Array<{ name: string; prefixes: string[]; description: string; bypassable: boolean }>; + bypassEnabled: boolean; + bypassPrefixes: string[]; + spawnCapablePrefixes: string[]; + }; + + assert.equal(body.tiers.length, 5); + const names = body.tiers.map((t) => t.name).sort(); + assert.deepEqual(names, ["ALWAYS_PROTECTED", "CLIENT_API", "LOCAL_ONLY", "MANAGEMENT", "PUBLIC"]); + + const localOnly = body.tiers.find((t) => t.name === "LOCAL_ONLY"); + assert.ok(localOnly); + assert.ok(localOnly!.prefixes.includes("/api/mcp/")); + assert.ok(localOnly!.prefixes.includes("/api/cli-tools/runtime/")); + assert.equal(localOnly!.bypassable, true); + + const alwaysProtected = body.tiers.find((t) => t.name === "ALWAYS_PROTECTED"); + assert.ok(alwaysProtected); + assert.ok(alwaysProtected!.prefixes.includes("/api/shutdown")); + assert.equal(alwaysProtected!.bypassable, false); + + // Every tier carries a non-empty description. + for (const tier of body.tiers) { + assert.ok(tier.description.length > 0, `tier ${tier.name} missing description`); + } + + assert.ok(body.spawnCapablePrefixes.includes("/api/cli-tools/runtime/")); +}); + +// ─── AC-2 — flags match getSettings() pre- and post-mutation ────────────── + +test("AC-2: bypassEnabled + bypassPrefixes reflect getSettings() (defaults)", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-ac2a"; + await settingsDb.updateSettings({ requireLogin: true }); + + const response = await inventoryRoute.GET( + await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { + method: "GET", + }) + ); + assert.equal(response.status, 200); + const body = (await response.json()) as { + bypassEnabled: boolean; + bypassPrefixes: string[]; + }; + // Default snapshot: kill-switch ON, single prefix /api/mcp/. + assert.equal(body.bypassEnabled, true); + assert.deepEqual(body.bypassPrefixes, ["/api/mcp/"]); +}); + +test("AC-2: bypassEnabled flips after settings mutation", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-ac2b"; + await settingsDb.updateSettings({ requireLogin: true }); + + // Mutate directly through the settings DB (bypasses the password gate — + // we are not testing the gate here, only the inventory's reflection of + // the persisted state). + await settingsDb.updateSettings({ localOnlyManageScopeBypassEnabled: false }); + + const response = await inventoryRoute.GET( + await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { + method: "GET", + }) + ); + assert.equal(response.status, 200); + const body = (await response.json()) as { bypassEnabled: boolean }; + assert.equal(body.bypassEnabled, false); +}); + +test("AC-2: bypassPrefixes additions land in the inventory", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-ac2c"; + await settingsDb.updateSettings({ requireLogin: true }); + + await settingsDb.updateSettings({ + localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/mcp/v2/"], + }); + + const response = await inventoryRoute.GET( + await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", { + method: "GET", + }) + ); + assert.equal(response.status, 200); + const body = (await response.json()) as { bypassPrefixes: string[] }; + assert.deepEqual(body.bypassPrefixes, ["/api/mcp/", "/api/mcp/v2/"]); +}); + +// ─── AC-12 — anonymous request rejected (no inventory leak) ─────────────── + +test("AC-12: anonymous request (no cookie, no Bearer) → 401", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-ac12"; + // Bootstrap a password so isAuthRequired() returns true even on loopback. + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + + const anonRequest = new Request("https://dashboard.example/api/settings/authz-inventory", { + method: "GET", + }); + const response = await inventoryRoute.GET(anonRequest); + assert.ok( + response.status === 401 || response.status === 403, + `expected 401/403, got ${response.status}` + ); + const body = (await response.json()) as { error?: { message?: string } }; + // Should NOT leak the inventory shape. + assert.ok(!("tiers" in body)); +}); + +test("AC-12: anonymous request with bogus Bearer → 403", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-ac12b"; + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + + const bogus = new Request("https://dashboard.example/api/settings/authz-inventory", { + method: "GET", + headers: new Headers({ authorization: "Bearer not-a-real-key" }), + }); + const response = await inventoryRoute.GET(bogus); + assert.equal(response.status, 403); +}); + +// ─── OQ-5 — any valid API key (no manage scope required) → 200 ──────────── + +test("OQ-5: any valid API key (read-only scope) → 200 inventory", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-inventory"; + process.env.INITIAL_PASSWORD = "initial-pass-oq5"; + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + + // Key with NO manage scope — would be rejected by /api/settings PATCH, + // but the inventory read endpoint is intentionally one rung lower (OQ-5). + const created = await apiKeysDb.createApiKey("oq5-read", "machine-oq5", []); + const request = new Request("https://dashboard.example/api/settings/authz-inventory", { + method: "GET", + headers: new Headers({ authorization: `Bearer ${created.key}` }), + }); + const response = await inventoryRoute.GET(request); + assert.equal(response.status, 200); + const body = (await response.json()) as { tiers: unknown[] }; + assert.equal(body.tiers.length, 5); +}); diff --git a/tests/unit/api/settings-audit.test.ts b/tests/unit/api/settings-audit.test.ts new file mode 100644 index 0000000000..e23296713b --- /dev/null +++ b/tests/unit/api/settings-audit.test.ts @@ -0,0 +1,307 @@ +/** + * T-012 — Settings PATCH audit log. + * + * Covers spec AC-9 / AC-10 / AC-11 (plus idempotent no-op case): + * - AC-9 success diff row carries `action=settings.update`, target, + * actor, ip, and per-key {before, after} diff for every changed key. + * - AC-10 each rejection path (PASSWORD_REQUIRED, PASSWORD_MISMATCH, + * BYPASS_PREFIX_NOT_ALLOWED, zod validation failure) writes a + * `settings.update_failed` row with the matching `reason` code and + * NEVER persists settings. + * - AC-11 every changed key shows up in the success diff — not only + * security-impacting keys. + * - Idempotent PATCH (body matches stored state) writes NO row. + * + * Runs through the real PATCH handler + `setupSettingsFixture` mock so the + * production `updateSettings → applyRuntimeSettings → logAuditEvent` pipeline + * fires exactly as it does in deployment. + * + * INSUFFICIENT_SCOPE is intentionally NOT exercised here — per spec AC-13 it + * is rejected by `requireManagementAuth` before the audit-aware handler body + * runs, so no row is written. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { setupSettingsFixture } from "../_mocks/settings.ts"; +import { makeManagementSessionRequest } from "../../helpers/managementSession.ts"; + +// Allocate fixture FIRST so DATA_DIR is set before any DB import resolves. +const fixture = setupSettingsFixture("settings-audit"); +process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1"; + +const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; +const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; + +const core = await import("../../../src/lib/db/core.ts"); +const settingsDb = await import("../../../src/lib/db/settings.ts"); +const runtime = await import("../../../src/lib/config/runtimeSettings.ts"); +const settingsRoute = await import("../../../src/app/api/settings/route.ts"); +const compliance = await import("../../../src/lib/compliance/index.ts"); +const managementPassword = await import("../../../src/lib/auth/managementPassword.ts"); +const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); + +test.beforeEach(async () => { + await fixture.resetStorage(); + apiKeysDb.resetApiKeyState(); + runtime.resetRuntimeSettingsStateForTests(); +}); + +test.after(() => { + core.resetDbInstance(); + fixture.cleanup(); + if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD; + else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; + if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET; +}); + +async function bootstrapWithPassword(password: string): Promise { + process.env.JWT_SECRET = "test-jwt-secret-settings-audit"; + process.env.INITIAL_PASSWORD = password; + await settingsDb.updateSettings({ requireLogin: true }); + await managementPassword.ensurePersistentManagementPasswordHash({ + source: "test.bootstrap", + }); +} + +function settingsRows() { + // `getAuditLog`'s `AuditLogEntry[]` return type now exposes `action`, + // `actor`, `target`, `status`, `details`, etc. directly — no local cast + // needed. See src/lib/compliance/index.ts. + return compliance.getAuditLog({ target: "settings", limit: 50 }); +} + +// ─── AC-9 — success diff row written ────────────────────────────────────── + +test("AC-9: successful PATCH writes settings.update with diff of changed keys", async () => { + await bootstrapWithPassword("initial-pass-ac9"); + const before = await settingsDb.getSettings(); + assert.equal(before.localOnlyManageScopeBypassEnabled, true); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassEnabled: false, + currentPassword: "initial-pass-ac9", + }, + }) + ); + + assert.equal(response.status, 200); + + const rows = settingsRows(); + const successRows = rows.filter((r) => r.action === "settings.update"); + assert.equal(successRows.length, 1, `expected 1 success row, got: ${JSON.stringify(rows)}`); + const row = successRows[0]; + assert.equal(row.target, "settings"); + assert.equal(row.status, "success"); + assert.equal(row.resource_type, "settings"); + // Cookie session ⇒ actor=dashboard. + assert.equal(row.actor, "dashboard"); + const details = row.details as { diff: Record }; + assert.ok(details && typeof details === "object", "details must be parsed JSON"); + assert.ok(details.diff, "diff present"); + assert.deepEqual(details.diff.localOnlyManageScopeBypassEnabled, { + before: true, + after: false, + }); +}); + +// ─── AC-10 — failure rows for each rejection path ──────────────────────── + +test("AC-10a: PASSWORD_REQUIRED failure writes settings.update_failed", async () => { + await bootstrapWithPassword("initial-pass-ac10a"); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { localOnlyManageScopeBypassEnabled: false }, + }) + ); + + assert.equal(response.status, 400); + const rows = settingsRows().filter((r) => r.action === "settings.update_failed"); + assert.equal(rows.length, 1); + const details = rows[0].details as { reason: string; attempted_keys: string[] }; + assert.equal(details.reason, "PASSWORD_REQUIRED"); + assert.ok(details.attempted_keys.includes("localOnlyManageScopeBypassEnabled")); + // No raw payload values — only the key NAMES are recorded under + // `attempted_keys`. There must be no `before`/`after` or `diff` block on a + // failure row, and no other fields beyond reason+attempted_keys in details. + assert.deepEqual( + Object.keys(details).sort(), + ["attempted_keys", "reason"], + "failure details must only contain reason + attempted_keys (no payload echo)" + ); + + // Persisted state unchanged. + const after = await settingsDb.getSettings(); + assert.equal(after.localOnlyManageScopeBypassEnabled, true); +}); + +test("AC-10b: PASSWORD_MISMATCH failure writes settings.update_failed", async () => { + await bootstrapWithPassword("initial-pass-ac10b"); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassEnabled: false, + currentPassword: "definitely-wrong", + }, + }) + ); + + assert.equal(response.status, 401); + const rows = settingsRows().filter((r) => r.action === "settings.update_failed"); + assert.equal(rows.length, 1); + const details = rows[0].details as { reason: string; attempted_keys: string[] }; + assert.equal(details.reason, "PASSWORD_MISMATCH"); + // Password attempt MUST NOT leak — only key names. + const serialized = JSON.stringify(rows[0]); + assert.equal( + serialized.includes("definitely-wrong"), + false, + "rejected currentPassword must not appear in audit row" + ); + + const after = await settingsDb.getSettings(); + assert.equal(after.localOnlyManageScopeBypassEnabled, true); +}); + +test("AC-10c: BYPASS_PREFIX_NOT_ALLOWED failure writes settings.update_failed", async () => { + await bootstrapWithPassword("initial-pass-ac10c"); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/cli-tools/runtime/"], + currentPassword: "initial-pass-ac10c", + }, + }) + ); + + assert.equal(response.status, 400); + const rows = settingsRows().filter((r) => r.action === "settings.update_failed"); + assert.equal(rows.length, 1); + const details = rows[0].details as { reason: string }; + assert.equal(details.reason, "BYPASS_PREFIX_NOT_ALLOWED"); + + // Snapshot untouched. + const after = await settingsDb.getSettings(); + assert.deepEqual(after.localOnlyManageScopeBypassPrefixes, ["/api/mcp/"]); +}); + +test("AC-10d: zod validation failure (wrong type) writes settings.update_failed", async () => { + await bootstrapWithPassword("initial-pass-ac10d"); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassEnabled: "definitely-not-a-boolean", + currentPassword: "initial-pass-ac10d", + }, + }) + ); + + assert.equal(response.status, 400); + const rows = settingsRows().filter((r) => r.action === "settings.update_failed"); + assert.equal(rows.length, 1); + const details = rows[0].details as { reason: string }; + assert.equal(details.reason, "VALIDATION_FAILED"); +}); + +// AC-13 sanity: INSUFFICIENT_SCOPE rejection happens upstream in +// requireManagementAuth and never reaches the handler body, so no audit row. +// We cover it implicitly by NOT having an INSUFFICIENT_SCOPE failure test — +// the route-level rejection is already covered by api-auth.test.ts. + +// ─── AC-11 — diff covers every changed key (not only security keys) ────── + +test("AC-11: diff records every changed key, including non-security keys", async () => { + await bootstrapWithPassword("initial-pass-ac11"); + + // Seed an initial value for a non-security key so the diff is meaningful. + await settingsDb.updateSettings({ theme: "light", instanceName: "before" }); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + theme: "dark", + instanceName: "after", + localOnlyManageScopeBypassEnabled: false, + currentPassword: "initial-pass-ac11", + }, + }) + ); + + assert.equal(response.status, 200); + const rows = settingsRows().filter((r) => r.action === "settings.update"); + assert.equal(rows.length, 1); + const details = rows[0].details as { diff: Record }; + // Security key AND multiple non-security keys must all be in diff. + assert.ok(details.diff.localOnlyManageScopeBypassEnabled, "security key in diff"); + assert.ok(details.diff.theme, "theme (non-security) in diff"); + assert.ok(details.diff.instanceName, "instanceName (non-security) in diff"); + assert.deepEqual(details.diff.theme, { before: "light", after: "dark" }); + assert.deepEqual(details.diff.instanceName, { before: "before", after: "after" }); +}); + +// ─── Idempotent no-op writes NO row ────────────────────────────────────── + +test("idempotent PATCH (body matches current state) writes NO audit row", async () => { + await bootstrapWithPassword("initial-pass-noop"); + + // Settings already at default — patch the same value back. + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassEnabled: true, // same as default + currentPassword: "initial-pass-noop", + }, + }) + ); + + assert.equal(response.status, 200); + const rows = settingsRows(); + assert.equal( + rows.length, + 0, + `idempotent PATCH must not emit an audit row, got: ${JSON.stringify(rows)}` + ); +}); + +// ─── Multi-row sanity: success + failure sequence ───────────────────────── + +test("sequence: failure then success produces exactly 1 failure row + 1 success row", async () => { + await bootstrapWithPassword("initial-pass-seq"); + + // 1) failure + await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { localOnlyManageScopeBypassEnabled: false, currentPassword: "wrong" }, + }) + ); + // 2) success + await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassEnabled: false, + currentPassword: "initial-pass-seq", + }, + }) + ); + + const rows = settingsRows(); + const failures = rows.filter((r) => r.action === "settings.update_failed"); + const successes = rows.filter((r) => r.action === "settings.update"); + assert.equal(failures.length, 1); + assert.equal(successes.length, 1); +}); diff --git a/tests/unit/authz/management-policy.test.ts b/tests/unit/authz/management-policy.test.ts index b75be34acf..e5d2961c30 100644 --- a/tests/unit/authz/management-policy.test.ts +++ b/tests/unit/authz/management-policy.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { SignJWT } from "jose"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-mgmt-policy-")); process.env.DATA_DIR = TEST_DATA_DIR; @@ -45,6 +46,24 @@ async function loadPolicy() { return mod.managementPolicy; } +async function dashboardCookieHeader(expiresIn = "1h"): Promise { + // Mirrors tests/unit/authz/pipeline.test.ts: mint a real HS256 auth_token + // JWT against process.env.JWT_SECRET so isDashboardSessionAuthenticated() + // accepts it. The header path is sufficient — the policy reads the cookie + // from `request.headers.get("cookie")` when there's no `request.cookies` + // accessor on the plain ctx() object. + assert.ok( + process.env.JWT_SECRET, + "JWT_SECRET must be set before minting dashboard cookie (otherwise TextEncoder would encode the string 'undefined' and silently mint a wrong-secret JWT)" + ); + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const token = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime(expiresIn) + .sign(secret); + return `auth_token=${token}`; +} + function ctx(headers: Headers, method = "GET", path = "/api/keys") { return { request: { method, headers, url: `http://localhost${path}`, nextUrl: { pathname: path } }, @@ -189,6 +208,155 @@ test("managementPolicy: rejects invalid API keys with 403 when bearer is present } }); +// ─── LOCAL_ONLY manage-scope bypass for /api/mcp/* ─────────────────────────── +// +// `/api/mcp/*` is in LOCAL_ONLY_API_PREFIXES (because it can spawn child +// processes for unauthenticated callers) AND in +// LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES (so a manage-scoped API key +// presented from non-loopback may reach it). `/api/cli-tools/runtime/*` is +// LOCAL_ONLY but NOT bypassable — the carve-out is path-scoped. +// +// `ctx()` uses `new Headers()` without an explicit `host`, so +// `isLoopbackHost(null)` returns false → the policy treats it as non-loopback, +// which is the exact case this block exercises. + +test("LOCAL_ONLY manage-scope bypass: no Bearer + non-loopback → 403 (regression guard)", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + + const policy = await loadPolicy(); + const out = await policy.evaluate(ctx(new Headers(), "GET", "/api/mcp/stream")); + + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 403); + assert.equal(out.code, "LOCAL_ONLY"); + } +}); + +test("LOCAL_ONLY manage-scope bypass: non-manage key + non-loopback → 403", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + const created = await apiKeysDb.createApiKey("chat-only", "machine-chat-only", ["chat"]); + + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ authorization: `Bearer ${created.key}` }), "GET", "/api/mcp/stream") + ); + + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 403); + assert.equal(out.code, "LOCAL_ONLY"); + } +}); + +test("LOCAL_ONLY manage-scope bypass: manage-scope key + non-loopback → allow", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + const created = await apiKeysDb.createApiKey("mcp-bypass-key", "machine-mcp-bypass", ["manage"]); + + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ authorization: `Bearer ${created.key}` }), "GET", "/api/mcp/stream") + ); + + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "management_key"); + assert.equal(out.subject.id, created.id); + assert.ok( + (out.subject.label ?? "").includes("local-only-bypass"), + `expected label to include 'local-only-bypass', got ${out.subject.label}` + ); + } +}); + +test("LOCAL_ONLY manage-scope bypass: carve-out does not extend to /api/cli-tools/runtime/*", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + const created = await apiKeysDb.createApiKey("cli-runtime-denied", "machine-cli-runtime-denied", [ + "manage", + ]); + + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx( + new Headers({ authorization: `Bearer ${created.key}` }), + "GET", + "/api/cli-tools/runtime/foo" + ) + ); + + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 403); + assert.equal(out.code, "LOCAL_ONLY"); + } +}); + +test("LOCAL_ONLY manage-scope bypass: loopback + no Bearer → allow (local CLI flow preserved)", async () => { + // Match the fresh-bootstrap pattern used by the "allows when auth not + // required" test above: no password configured + loopback request → + // `isAuthRequired` returns false → anonymous-allow fires once the LOCAL_ONLY + // gate is satisfied via the loopback `host` header. + await settingsDb.updateSettings({ requireLogin: true, password: null }); + + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ host: "localhost:20128" }), "GET", "/api/mcp/stream") + ); + + assert.equal(out.allow, true); +}); + +// ─── LOCAL_ONLY dashboard-session bypass ───────────────────────────────────── +// +// Regression cover for commit ca284a91 ("refine LOCAL_ONLY bypass — dashboard +// cookie + admin label + error log"). The dashboard-session bypass mirrors the +// manage-scope bypass: an authenticated `auth_token` cookie reaching a +// bypassable LOCAL_ONLY path (e.g. /api/mcp/status) from a public hostname is +// allowed, but the cli-tools-runtime carve-out is NOT extended to it. + +test("LOCAL_ONLY dashboard-session bypass: authenticated dashboard cookie + non-loopback → allow", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + + const cookie = await dashboardCookieHeader(); + const policy = await loadPolicy(); + const out = await policy.evaluate(ctx(new Headers({ cookie }), "GET", "/api/mcp/stream")); + + assert.equal(out.allow, true); + if (out.allow) { + assert.equal(out.subject.kind, "dashboard_session"); + assert.equal(out.subject.id, "dashboard"); + assert.equal(out.subject.label, "dashboard-session-local-only-bypass"); + } +}); + +test("LOCAL_ONLY dashboard-session bypass: authenticated dashboard cookie + /api/cli-tools/runtime/ → 403 LOCAL_ONLY", async () => { + process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; + process.env.INITIAL_PASSWORD = "initial-pass"; + await settingsDb.updateSettings({ requireLogin: true }); + + const cookie = await dashboardCookieHeader(); + const policy = await loadPolicy(); + const out = await policy.evaluate( + ctx(new Headers({ cookie }), "GET", "/api/cli-tools/runtime/foo") + ); + + assert.equal(out.allow, false); + if (!out.allow) { + assert.equal(out.status, 403); + assert.equal(out.code, "LOCAL_ONLY"); + } +}); + test("managementPolicy: allows internal model sync only on the dedicated provider routes", async () => { process.env.JWT_SECRET = "test-jwt-secret-for-mgmt-policy"; process.env.INITIAL_PASSWORD = "initial-pass"; diff --git a/tests/unit/authz/routeGuard.test.ts b/tests/unit/authz/routeGuard.test.ts index d9af73e666..99e6fec9e3 100644 --- a/tests/unit/authz/routeGuard.test.ts +++ b/tests/unit/authz/routeGuard.test.ts @@ -2,6 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { isLocalOnlyPath, + isLocalOnlyBypassableByManageScope, isAlwaysProtectedPath, isLoopbackHost, } from "../../../src/server/authz/routeGuard.ts"; @@ -25,6 +26,19 @@ test("isLocalOnlyPath: regular management routes are not local-only", () => { assert.equal(isLocalOnlyPath("/api/providers"), false); }); +test("isLocalOnlyBypassableByManageScope: /api/mcp/ prefix is bypassable", () => { + assert.equal(isLocalOnlyBypassableByManageScope("/api/mcp/"), true); + assert.equal(isLocalOnlyBypassableByManageScope("/api/mcp/stream"), true); +}); + +test("isLocalOnlyBypassableByManageScope: /api/cli-tools/runtime/* is NOT bypassable", () => { + assert.equal(isLocalOnlyBypassableByManageScope("/api/cli-tools/runtime/foo"), false); +}); + +test("isLocalOnlyBypassableByManageScope: non-local-only routes are not bypassable", () => { + assert.equal(isLocalOnlyBypassableByManageScope("/api/settings"), false); +}); + test("isAlwaysProtectedPath: /api/shutdown is always protected", () => { assert.equal(isAlwaysProtectedPath("/api/shutdown"), true); }); diff --git a/tests/unit/db/api-keys.test.ts b/tests/unit/db/api-keys.test.ts new file mode 100644 index 0000000000..3c01698b56 --- /dev/null +++ b/tests/unit/db/api-keys.test.ts @@ -0,0 +1,242 @@ +/** + * Unit coverage for the api_keys DB layer — focused on the `scopes` column + * and the audit-event emission contract for privileged scope changes. + * + * The fixtures here intentionally mirror tests/unit/api-auth.test.ts so the + * two suites share the same bootstrap shape (isolated DATA_DIR, fresh DB per + * test, env reset). + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-api-keys-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = "test-api-key-secret"; + +const core = await import("../../../src/lib/db/core.ts"); +const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); +const compliance = await import("../../../src/lib/compliance/index.ts"); +const { hasManageScope } = await import("../../../src/shared/constants/managementScopes.ts"); + +const MACHINE_ID = "machine1234567890"; + +async function resetStorage() { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(() => { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// createApiKey + scopes round-trip +// ───────────────────────────────────────────────────────────────────────────── + +test("createApiKey persists scopes to the api_keys row", async () => { + const created = await apiKeysDb.createApiKey("with-manage", MACHINE_ID, ["manage"]); + assert.ok(created.id); + assert.ok(created.key); + assert.deepEqual(created.scopes, ["manage"]); + + // Verify the row hit the DB by reading raw column. + const db = core.getDbInstance() as unknown as { + prepare: (sql: string) => { get: (id: string) => { scopes: string | null } | undefined }; + }; + const row = db.prepare("SELECT scopes FROM api_keys WHERE id = ?").get(created.id); + assert.equal(row?.scopes, JSON.stringify(["manage"])); +}); + +test("createApiKey with default scopes writes an empty JSON array", async () => { + const created = await apiKeysDb.createApiKey("no-scope", MACHINE_ID); + const db = core.getDbInstance() as unknown as { + prepare: (sql: string) => { get: (id: string) => { scopes: string | null } | undefined }; + }; + const row = db.prepare("SELECT scopes FROM api_keys WHERE id = ?").get(created.id); + assert.equal(row?.scopes, "[]"); +}); + +test("getApiKeyMetadata returns the scopes for a key created with manage", async () => { + const created = await apiKeysDb.createApiKey("metadata-readback", MACHINE_ID, ["manage"]); + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.deepEqual(meta!.scopes, ["manage"]); + assert.equal(hasManageScope(meta!.scopes), true); +}); + +test("getApiKeyMetadata returns an empty scopes array for a key created without scopes", async () => { + const created = await apiKeysDb.createApiKey("no-manage", MACHINE_ID); + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.deepEqual(meta!.scopes, []); + assert.equal(hasManageScope(meta!.scopes), false); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Legacy NULL scopes (pre-migration-032 row simulation) +// ───────────────────────────────────────────────────────────────────────────── + +test("legacy rows with NULL scopes parse to an empty array and never hold manage", async () => { + const created = await apiKeysDb.createApiKey("legacy-null", MACHINE_ID); + // Simulate a pre-migration row by force-NULL on the scopes column. + const db = core.getDbInstance() as unknown as { + prepare: (sql: string) => { run: (...args: unknown[]) => unknown }; + }; + db.prepare("UPDATE api_keys SET scopes = NULL WHERE id = ?").run(created.id); + apiKeysDb.clearApiKeyCaches(); + + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.deepEqual(meta!.scopes, []); + assert.equal(hasManageScope(meta!.scopes), false); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// updateApiKeyPermissions — audit events for scope changes +// ───────────────────────────────────────────────────────────────────────────── + +test("updateApiKeyPermissions granting manage emits apiKey.scopes.grant", async () => { + const created = await apiKeysDb.createApiKey("for-grant", MACHINE_ID); + + const before = compliance.getAuditLog({ limit: 100 }); + const beforeGrant = before.filter( + (e) => e.action === "apiKey.scopes.grant" && e.target === created.id + ); + assert.equal(beforeGrant.length, 0); + + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { scopes: ["manage"] }); + assert.equal(ok, true); + + const after = compliance.getAuditLog({ limit: 100 }); + const grants = after.filter((e) => e.action === "apiKey.scopes.grant" && e.target === created.id); + assert.equal(grants.length, 1, "expected exactly one grant audit event"); + + // Confirm the round-trip — manage should now be on the key. + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.equal(hasManageScope(meta!.scopes), true); +}); + +test("updateApiKeyPermissions revoking manage emits apiKey.scopes.revoke", async () => { + const created = await apiKeysDb.createApiKey("for-revoke", MACHINE_ID, ["manage"]); + + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { scopes: [] }); + assert.equal(ok, true); + + const after = compliance.getAuditLog({ limit: 100 }); + const revokes = after.filter( + (e) => e.action === "apiKey.scopes.revoke" && e.target === created.id + ); + assert.equal(revokes.length, 1, "expected exactly one revoke audit event"); + + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.equal(hasManageScope(meta!.scopes), false); +}); + +test("updateApiKeyPermissions setting same manage scope does not emit duplicate audit events", async () => { + const created = await apiKeysDb.createApiKey("idempotent-manage", MACHINE_ID, ["manage"]); + + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { scopes: ["manage"] }); + assert.equal(ok, true); + + const after = compliance.getAuditLog({ limit: 100 }); + const scopeEvents = after.filter( + (e) => + (e.action === "apiKey.scopes.grant" || + e.action === "apiKey.scopes.revoke" || + e.action === "apiKey.scopes.update") && + e.target === created.id + ); + assert.equal( + scopeEvents.length, + 0, + "no-op scope update should not emit grant/revoke/update events" + ); +}); + +test("updateApiKeyPermissions changing non-manage scopes emits apiKey.scopes.update", async () => { + const created = await apiKeysDb.createApiKey("non-manage-update", MACHINE_ID, []); + + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { scopes: ["read:logs"] }); + assert.equal(ok, true); + + const after = compliance.getAuditLog({ limit: 100 }); + const updates = after.filter( + (e) => e.action === "apiKey.scopes.update" && e.target === created.id + ); + assert.equal(updates.length, 1, "expected exactly one non-manage scope update event"); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Regression — scopes and ban state are orthogonal (T-008) +// +// The Permissions modal previously had two chips bound to `isBanned` (one +// labelled "Management API Access" with manage-scope copy). A user toggling +// it expected manage-scope grant; instead it flipped the ban flag. These +// tests guard against the inverse cross-wire ever returning: updating scopes +// must not touch isBanned, and toggling isBanned must not touch scopes. +// ───────────────────────────────────────────────────────────────────────────── + +test("updating scopes to manage leaves isBanned untouched", async () => { + const created = await apiKeysDb.createApiKey("banned-then-manage", MACHINE_ID, []); + const banOk = await apiKeysDb.updateApiKeyPermissions(created.id, { isBanned: true }); + assert.equal(banOk, true); + + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { scopes: ["manage"] }); + assert.equal(ok, true); + + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.deepEqual(meta!.scopes, ["manage"]); + assert.equal(meta!.isBanned, true, "ban flag must survive a scopes-only update"); +}); + +test("toggling isBanned does not touch scopes", async () => { + const created = await apiKeysDb.createApiKey("manage-then-ban", MACHINE_ID, ["manage"]); + + const banOk = await apiKeysDb.updateApiKeyPermissions(created.id, { isBanned: true }); + assert.equal(banOk, true); + + const meta = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta); + assert.deepEqual(meta!.scopes, ["manage"], "scopes must survive a ban-only update"); + assert.equal(meta!.isBanned, true); + + const unbanOk = await apiKeysDb.updateApiKeyPermissions(created.id, { isBanned: false }); + assert.equal(unbanOk, true); + + const meta2 = await apiKeysDb.getApiKeyMetadata(created.key); + assert.ok(meta2); + assert.deepEqual(meta2!.scopes, ["manage"], "scopes must survive an unban update"); + assert.equal(meta2!.isBanned, false); +}); + +test("updateApiKeyPermissions without scopes field does not emit any scope audit event", async () => { + const created = await apiKeysDb.createApiKey("no-scope-change", MACHINE_ID); + + const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { name: "renamed" }); + assert.equal(ok, true); + + const after = compliance.getAuditLog({ limit: 100 }); + const scopeEvents = after.filter( + (e) => + (e.action === "apiKey.scopes.grant" || + e.action === "apiKey.scopes.revoke" || + e.action === "apiKey.scopes.update") && + e.target === created.id + ); + assert.equal(scopeEvents.length, 0); +}); diff --git a/tests/unit/settings-route-password.test.ts b/tests/unit/settings-route-password.test.ts index 3b0fee671a..32f967f2ce 100644 --- a/tests/unit/settings-route-password.test.ts +++ b/tests/unit/settings-route-password.test.ts @@ -79,5 +79,8 @@ test("settings route password update rejects the wrong current password after mi ); assert.equal(response.status, 401); - assert.deepEqual(await response.json(), { error: "Invalid current password" }); + // T-011 unified security-impacting gate: structured error code. + assert.deepEqual(await response.json(), { + error: { code: "PASSWORD_MISMATCH", message: "Invalid current password" }, + }); }); diff --git a/tests/unit/settings/authz-bypass.test.ts b/tests/unit/settings/authz-bypass.test.ts new file mode 100644 index 0000000000..1ba6302764 --- /dev/null +++ b/tests/unit/settings/authz-bypass.test.ts @@ -0,0 +1,286 @@ +/** + * T-011 — DB-stored authz bypass policy + hot-reload. + * + * Covers spec AC-3 through AC-8: + * - AC-3 kill-switch flip → bypass disabled → /api/mcp/ from non-loopback → 403 + * - AC-4 PATCH missing currentPassword → 400 PASSWORD_REQUIRED + * - AC-5 wrong currentPassword → 401 PASSWORD_MISMATCH + * - AC-6 toggle list reflects DB after PATCH + * - AC-7 add a new prefix → persists + applyRuntimeSettings fires + snapshot reflects + * - AC-8 add /api/cli-tools/runtime/ → 400 BYPASS_PREFIX_NOT_ALLOWED, snapshot unchanged + * + * Goes through the production `updateSettings → applyRuntimeSettings` and + * the real PATCH route handler — no direct `getAuthzBypassSnapshot` mocks. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { setupSettingsFixture, mockSettings } from "../_mocks/settings.ts"; +import { makeManagementSessionRequest } from "../../helpers/managementSession.ts"; + +// Allocate fixture FIRST so DATA_DIR is set before any DB import resolves. +const fixture = setupSettingsFixture("authz-bypass"); +// API-key auth check uses a Redis-backed cache otherwise — disable so +// isValidApiKey() does not stall on ETIMEDOUT in the local test loop. +process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1"; + +const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; +const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET; + +const core = await import("../../../src/lib/db/core.ts"); +const settingsDb = await import("../../../src/lib/db/settings.ts"); +const runtime = await import("../../../src/lib/config/runtimeSettings.ts"); +const routeGuard = await import("../../../src/server/authz/routeGuard.ts"); +const settingsRoute = await import("../../../src/app/api/settings/route.ts"); +const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts"); + +test.beforeEach(async () => { + await fixture.resetStorage(); + apiKeysDb.resetApiKeyState(); + // Force the route guard to start each test from cold-boot default + // (enabled=true, prefixes=["/api/mcp/"]). + runtime.resetRuntimeSettingsStateForTests(); +}); + +test.after(() => { + core.resetDbInstance(); + fixture.cleanup(); + if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD; + else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; + if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET; +}); + +function nonLoopbackCtx(headers: Headers, path = "/api/mcp/stream") { + return { + request: { + method: "GET", + headers, + url: `https://dashboard.example${path}`, + nextUrl: { pathname: path }, + }, + classification: { + routeClass: "MANAGEMENT" as const, + normalizedPath: path, + reason: "management_api" as const, + }, + requestId: "req_authz_bypass_test", + }; +} + +// ─── AC-3 — kill-switch flips bypass off → request 403 ─────────────────── + +test("AC-3: kill-switch off → /api/mcp/* with manage-scope Bearer from non-loopback → 403 LOCAL_ONLY", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-bypass"; + process.env.INITIAL_PASSWORD = "initial-pass"; + // Seed DB with default snapshot first (kill-switch ON) and confirm the + // bypass works. + await mockSettings({ requireLogin: true }); + const managePolicy = await import("../../../src/server/authz/policies/management.ts"); + const created = await apiKeysDb.createApiKey("ac3-mgmt", "machine-ac3", ["manage"]); + + const headers = new Headers({ authorization: `Bearer ${created.key}` }); + + // Sanity: bypass ENABLED → 200/allow. + const before = await managePolicy.managementPolicy.evaluate(nonLoopbackCtx(headers)); + assert.equal(before.allow, true, "default kill-switch ON should allow manage-scope bypass"); + + // Flip the kill-switch off via the production pipeline. + await mockSettings({ localOnlyManageScopeBypassEnabled: false }); + assert.equal(routeGuard.isLocalOnlyBypassableByManageScope("/api/mcp/stream"), false); + + // After the hot-reload, the policy must reject. + const after = await managePolicy.managementPolicy.evaluate(nonLoopbackCtx(headers)); + assert.equal(after.allow, false); + if (!after.allow) { + assert.equal(after.status, 403); + assert.equal(after.code, "LOCAL_ONLY"); + } +}); + +// ─── AC-4 — missing currentPassword → 400 PASSWORD_REQUIRED ────────────── + +test("AC-4: PATCH missing currentPassword for security-impacting key → 400 PASSWORD_REQUIRED", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-bypass"; + process.env.INITIAL_PASSWORD = "initial-pass-ac4"; + // Bootstrap a password so the cold-boot exception does NOT fire. + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { localOnlyManageScopeBypassEnabled: false }, + }) + ); + + assert.equal(response.status, 400); + const body = (await response.json()) as { error: { code: string; keys: string[] } }; + assert.equal(body.error.code, "PASSWORD_REQUIRED"); + assert.ok(body.error.keys.includes("localOnlyManageScopeBypassEnabled")); + + // Persisted state unchanged — kill-switch still on by default. + const settings = await settingsDb.getSettings(); + assert.equal(settings.localOnlyManageScopeBypassEnabled, true); +}); + +// ─── AC-5 — wrong currentPassword → 401 PASSWORD_MISMATCH ──────────────── + +test("AC-5: PATCH wrong currentPassword for security-impacting key → 401 PASSWORD_MISMATCH", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-bypass"; + process.env.INITIAL_PASSWORD = "initial-pass-ac5"; + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassEnabled: false, + currentPassword: "definitely-wrong", + }, + }) + ); + + assert.equal(response.status, 401); + const body = (await response.json()) as { error: { code: string } }; + assert.equal(body.error.code, "PASSWORD_MISMATCH"); + + const settings = await settingsDb.getSettings(); + assert.equal(settings.localOnlyManageScopeBypassEnabled, true); +}); + +// ─── AC-6 — bypass list reflects DB after PATCH ────────────────────────── + +test("AC-6: PATCH with correct currentPassword + new prefix list → persists to DB", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-bypass"; + process.env.INITIAL_PASSWORD = "initial-pass-ac6"; + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassPrefixes: ["/api/mcp/"], + currentPassword: "initial-pass-ac6", + }, + }) + ); + + assert.equal(response.status, 200); + const settings = await settingsDb.getSettings(); + assert.deepEqual(settings.localOnlyManageScopeBypassPrefixes, ["/api/mcp/"]); +}); + +// ─── AC-7 — add prefix → applyRuntimeSettings fires + snapshot reflects ── + +test("AC-7: PATCH adds prefix → applyRuntimeSettings fires + getAuthzBypassSnapshot reflects (hot-reload <50ms)", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-bypass"; + process.env.INITIAL_PASSWORD = "initial-pass-ac7"; + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + // Prime the snapshot from the persisted defaults so we measure a real diff. + const seeded = await settingsDb.getSettings(); + await runtime.applyRuntimeSettings(seeded); + + const before = routeGuard.LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES; + assert.deepEqual([...before], ["/api/mcp/"]); + + // Measure the snapshot-read latency (spec SLA: <50 ms). + const t0 = process.hrtime.bigint(); + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/mcp/v2/"], + currentPassword: "initial-pass-ac7", + }, + }) + ); + const snapshotAfterPatch = runtime.getAuthzBypassSnapshot(); + const elapsedNs = process.hrtime.bigint() - t0; + + assert.equal(response.status, 200); + assert.deepEqual(snapshotAfterPatch.prefixes, ["/api/mcp/", "/api/mcp/v2/"]); + assert.equal(snapshotAfterPatch.enabled, true); + // The hot-path accessor itself is O(1) — total PATCH→snapshot covers + // bcrypt + SQLite write, so we only assert the in-memory accessor is fast. + // Microbench: dedicated snapshot read. + const tSnap0 = process.hrtime.bigint(); + for (let i = 0; i < 10_000; i++) runtime.getAuthzBypassSnapshot(); + const snapElapsedNs = process.hrtime.bigint() - tSnap0; + assert.ok( + snapElapsedNs < 50_000_000n, + `getAuthzBypassSnapshot x10k must complete in <50 ms (got ${Number(snapElapsedNs) / 1e6} ms)` + ); + // Whole PATCH < 5 s sanity bound (bcrypt-bounded). + assert.ok(elapsedNs < 5_000_000_000n); + + // Live route-guard predicate reflects the new prefix. + assert.equal(routeGuard.isLocalOnlyBypassableByManageScope("/api/mcp/v2/foo"), true); +}); + +// ─── AC-8 — spawn-capable prefix → 400 BYPASS_PREFIX_NOT_ALLOWED ───────── + +test("AC-8: PATCH with /api/cli-tools/runtime/ in bypass list → 400 BYPASS_PREFIX_NOT_ALLOWED + snapshot unchanged", async () => { + process.env.JWT_SECRET = "test-jwt-secret-authz-bypass"; + process.env.INITIAL_PASSWORD = "initial-pass-ac8"; + await settingsDb.updateSettings({ requireLogin: true }); + const { ensurePersistentManagementPasswordHash } = + await import("../../../src/lib/auth/managementPassword.ts"); + await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" }); + // Prime snapshot from default DB state. + const seeded = await settingsDb.getSettings(); + await runtime.applyRuntimeSettings(seeded); + const snapshotBefore = runtime.getAuthzBypassSnapshot(); + + const response = await settingsRoute.PATCH( + await makeManagementSessionRequest("http://localhost/api/settings", { + method: "PATCH", + body: { + localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/cli-tools/runtime/"], + currentPassword: "initial-pass-ac8", + }, + }) + ); + + assert.equal(response.status, 400); + const body = (await response.json()) as { + error: { details?: Array<{ field: string; message: string }> }; + }; + // zod path is the array path; the message embeds the BYPASS_PREFIX_NOT_ALLOWED code. + const offending = body.error.details?.find((d) => + d.message.includes("BYPASS_PREFIX_NOT_ALLOWED") + ); + assert.ok(offending, `expected BYPASS_PREFIX_NOT_ALLOWED in details: ${JSON.stringify(body)}`); + + // Persisted state untouched. + const settings = await settingsDb.getSettings(); + assert.deepEqual(settings.localOnlyManageScopeBypassPrefixes, ["/api/mcp/"]); + // Runtime snapshot untouched. + const snapshotAfter = runtime.getAuthzBypassSnapshot(); + assert.deepEqual(snapshotAfter.prefixes, snapshotBefore.prefixes); + assert.equal(snapshotAfter.enabled, snapshotBefore.enabled); +}); + +// ─── Defence-in-depth: snapshot mutation alone cannot grant spawn bypass ─ + +test("Defence-in-depth: even if a malformed snapshot lists /api/cli-tools/runtime/, the runtime predicate rejects it", async () => { + // applyRuntimeSettings wires the snapshot through normalizeAuthzBypass, + // which does not filter spawn-capable entries (zod is the gate). The + // routeGuard predicate must still refuse them at runtime. + await runtime.applyRuntimeSettings({ + localOnlyManageScopeBypassEnabled: true, + localOnlyManageScopeBypassPrefixes: ["/api/cli-tools/runtime/"], + }); + + assert.equal(routeGuard.isLocalOnlyBypassableByManageScope("/api/cli-tools/runtime/foo"), false); +}); From aa72129f64058e4f63790a31966041f07f159369 Mon Sep 17 00:00:00 2001 From: Apostol Apostolov Date: Fri, 22 May 2026 00:23:58 +0300 Subject: [PATCH 046/112] feat(hermes): Add rich multi-role Hermes Agent support (#2526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(hermes): Add rich multi-role Hermes Agent support — integrated into release/v3.8.2 --- .gitignore | 1 + .../cli-tools/CLIToolsPageClient.tsx | 12 + .../components/HermesAgentToolCard.tsx | 513 ++++++++++++++++++ .../dashboard/cli-tools/components/index.tsx | 1 + .../(dashboard)/home/ProviderQuotaWidget.tsx | 196 +++++++ .../guide-settings/[toolId]/route.ts | 72 ++- .../cli-tools/hermes-agent-settings/route.ts | 115 ++++ src/app/api/cli-tools/status/route.ts | 11 +- src/i18n/messages/en.json | 2 + .../config-generator/hermes-agent.ts | 206 +++++++ src/lib/cli-helper/config-generator/hermes.ts | 47 ++ src/lib/cli-helper/config-generator/index.ts | 12 +- src/lib/cli-helper/tool-detector.ts | 47 +- src/shared/components/PwaRegister.tsx | 5 + src/shared/constants/cliTools.ts | 13 + src/shared/constants/homeWidgets.ts | 5 + src/shared/services/cliRuntime.ts | 11 + .../unit/cli-helper/config-generator.test.ts | 110 +++- tests/unit/cli-helper/tool-detector.test.ts | 15 + tests/unit/guide-settings-route.test.ts | 23 + 20 files changed, 1410 insertions(+), 7 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/cli-tools/components/HermesAgentToolCard.tsx create mode 100644 src/app/(dashboard)/home/ProviderQuotaWidget.tsx create mode 100644 src/app/api/cli-tools/hermes-agent-settings/route.ts create mode 100644 src/lib/cli-helper/config-generator/hermes-agent.ts create mode 100644 src/lib/cli-helper/config-generator/hermes.ts create mode 100644 src/shared/constants/homeWidgets.ts diff --git a/.gitignore b/.gitignore index 57c9a57613..bcb88085ee 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ app.log # Backup directories app.__qa_backup/ .app-build-backup-*/ +backup/ # Production standalone build (created by scripts/prepublish.mjs) # Conflicts with Next.js App Router detection in dev (root app/ shadows src/app/) diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx index f07d82e8da..fc9a6c6586 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx @@ -19,6 +19,7 @@ import { AntigravityToolCard, CopilotToolCard, CustomCliCard, + HermesAgentToolCard, } from "./components"; import { useTranslations } from "next-intl"; import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks"; @@ -32,6 +33,7 @@ const AUTO_CONFIGURED_TOOL_IDS = new Set([ "cline", "kilo", "copilot", + "hermes-agent", ]); const GUIDED_TOOL_IDS = new Set([ "cursor", @@ -349,6 +351,16 @@ export default function CLIToolsPageClient({ machineId: _machineId }) { cloudEnabled={cloudEnabled} /> ); + case "hermes-agent": + return ( + + ); case "custom": return ( >({}); + const [currentRoles, setCurrentRoles] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [message, setMessage] = useState(null); + const [modalRole, setModalRole] = useState(null); + const [previewYaml, setPreviewYaml] = useState(null); + const [isPreviewLoading, setIsPreviewLoading] = useState(false); + const [firstSetupAt, setFirstSetupAt] = useState(null); + + // Track whether we have already seeded from batchStatus on this expand + const seededFromBatchRef = useRef(false); + + function formatTimeSince(iso: string): string { + const then = new Date(iso).getTime(); + const diff = Date.now() - then; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + if (days > 0) return `${days}d`; + const hours = Math.floor(diff / (1000 * 60 * 60)); + if (hours > 0) return `${hours}h`; + const minutes = Math.floor(diff / (1000 * 60)); + return `${minutes}m`; + } + + useEffect(() => { + if (isExpanded) { + // Phase 3: Seed from detector snapshot (batchStatus) for instant UI + if (!seededFromBatchRef.current && + Object.keys(currentRoles).length === 0 && + batchStatus?.hermesAgentRoles) { + const seeded: Record = {}; + Object.entries(batchStatus.hermesAgentRoles).forEach(([role, info]: [string, any]) => { + seeded[role] = { + model: info.model, + provider: info.provider, + }; + }); + setCurrentRoles(seeded); + seededFromBatchRef.current = true; + } + loadCurrentConfig(); + } else { + // Reset seed flag when collapsed so it can seed again on next expand + seededFromBatchRef.current = false; + setPreviewYaml(null); + setFirstSetupAt(null); + } + }, [isExpanded, batchStatus, currentRoles]); + + const loadCurrentConfig = async () => { + setIsLoading(true); + try { + const res = await fetch("/api/cli-tools/hermes-agent-settings"); + const data = await res.json(); + if (data.success && data.roles) { + setCurrentRoles(data.roles); + if (data.firstSetupAt) setFirstSetupAt(data.firstSetupAt); + // Do NOT seed selections from disk data. + // selections only holds explicit user choices made in this session. + // Display falls back to currentRoles for unchanged roles. + } + } catch (e) { + console.warn("Could not load current Hermes Agent config", e); + } finally { + setIsLoading(false); + } + }; + + const setRoleSelection = (roleId: string, model: string, provider = "OmniRoute") => { + setSelections((prev) => ({ ...prev, [roleId]: { model, provider } })); + }; + + const applyToAll = (model: string) => { + const newSel: Record = {}; + HERMES_ROLES.forEach((r) => (newSel[r.id] = { model, provider: "OmniRoute" })); + setSelections(newSel); + }; + + const handleTogglePreview = async () => { + setMessage(null); + + // If preview is currently visible, hide it (toggle behavior) + if (previewYaml) { + setPreviewYaml(null); + return; + } + + // Build payload: prefer pending selections, fall back to currently loaded roles + let payloadSelections: Array<{ role: string; model: string }>; + + if (Object.keys(selections).length > 0) { + payloadSelections = Object.entries(selections).map(([role, sel]) => ({ + role, + model: sel.model, + })); + } else { + payloadSelections = Object.entries(currentRoles) + .filter(([_, info]) => info && info.model) + .map(([role, info]) => ({ role, model: info.model })); + } + + if (payloadSelections.length === 0) { + setMessage("Select models for roles (or ensure roles are loaded) before previewing."); + return; + } + + setIsPreviewLoading(true); + try { + const res = await fetch("/api/cli-tools/hermes-agent-settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + baseUrl, + keyId: apiKeys?.[0]?.id, + selections: payloadSelections, + preview: true, + }), + }); + + const data = await res.json(); + if (res.ok && data.yaml) { + setPreviewYaml(data.yaml); + } else { + setMessage(data.error || "Failed to generate preview"); + } + } catch { + setMessage("Failed to generate preview"); + } finally { + setIsPreviewLoading(false); + } + }; + + const handleSave = async () => { + setIsSaving(true); + setMessage(null); + + const payloadSelections = Object.entries(selections).map(([role, sel]) => ({ + role, + model: sel.model, + })); + + try { + const res = await fetch("/api/cli-tools/hermes-agent-settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + baseUrl, + keyId: apiKeys?.[0]?.id, + selections: payloadSelections, + }), + }); + + const data = await res.json(); + if (res.ok) { + setMessage(`Saved to ${data.configPath}`); + setSelections({}); // clear pending user choices after successful save + setPreviewYaml(null); // hide any open preview after apply + await loadCurrentConfig(); + } else { + setMessage(data.error || "Failed to save"); + } + } catch { + setMessage("Network error"); + } finally { + setIsSaving(false); + } + }; + + const isLoadingAny = isLoading || isSaving; + + // Effective per-role data for count + collapsed status. + // Priority: pending selections > freshly loaded currentRoles > batchStatus from detector (phase 3) + const effectiveRoles = React.useMemo(() => { + // If user has pending changes, treat selected roles as OmniRoute + if (Object.keys(selections).length > 0) { + const map: Record = {}; + HERMES_ROLES.forEach((r) => { + if (selections[r.id]) { + map[r.id] = { usingOmniRoute: true }; + } else if (currentRoles[r.id]) { + map[r.id] = currentRoles[r.id]; + } else if (batchStatus?.hermesAgentRoles?.[r.id]) { + map[r.id] = batchStatus.hermesAgentRoles[r.id]; + } + }); + return map; + } + + // Prefer fresh loaded data + if (Object.keys(currentRoles).length > 0) { + return currentRoles; + } + + // Fall back to detector snapshot (this is what finishes phase 3) + return batchStatus?.hermesAgentRoles || {}; + }, [selections, currentRoles, batchStatus]); + + // Count of roles that are (or will be) routed via OmniRoute + const configuredRolesCount = HERMES_ROLES.filter((role) => { + // Pending selection always counts as OmniRoute intent + if (selections[role.id]) return true; + + const info = effectiveRoles[role.id]; + if (!info) return false; + + // Support both shapes: detector shape (usingOmniRoute) and settings shape (provider + base_url) + if (typeof info.usingOmniRoute === "boolean") { + return info.usingOmniRoute; + } + return ( + info?.provider === "omniroute" || + (info?.base_url || "").includes("20128") || + (info?.base_url || "").includes("localhost") + ); + }).length; + + return ( + + {/* Collapsed header — exact match to OpenClaw / Kilo / other Auto-Configured entries */} +
+
+
+ terminal +
+
+
+

+ {tool?.name || "Hermes Agent"} + {firstSetupAt && ( + + schedule + {formatTimeSince(firstSetupAt)} since setup + + )} +

+ {(Object.keys(currentRoles).length > 0 || + Object.keys(selections).length > 0 || + Object.keys(batchStatus?.hermesAgentRoles || {}).length > 0) && ( + + {configuredRolesCount}/{HERMES_ROLES.length} roles + + )} +
+

+ {tool?.description || "Advanced multi-role terminal agent (by Nousresearch)"} +

+
+
+ + expand_more + +
+ + {isExpanded && ( +
+ {/* Right-aligned Refresh button (no counter) */} +
+ +
+ + {/* Quick apply row — consistent small action pills */} + {activeProviders?.[0]?.models?.length > 0 && ( +
+ Quick apply same model to all roles: + {activeProviders[0].models.slice(0, 6).map((m: any) => { + const modelValue = typeof m === "string" ? m : m?.value || m?.name; + if (!modelValue) return null; + return ( + + ); + })} +
+ )} + + {/* Roles list — flat consistent rows (no nested Card.Section boxes) */} +
+ {HERMES_ROLES.map((role) => { + const current = currentRoles[role.id]; + const sel = selections[role.id]; + + // displayed model prefers pending user choice, falls back to real current from YAML + const displayedModel = sel?.model || current?.model; + + // Badge logic per user's spec: + // - If user has selected something in this session (pending): show as via OmniRoute + // - Else if current from disk: show real provider name + "(not OmniRoute)" or "OmniRoute" + let badge: { label: string; pending: boolean } | null = null; + + if (sel) { + // pending change made via the Select modal / quick apply → will be routed via OmniRoute + const prov = sel.provider || "OmniRoute"; + badge = { label: `${prov} (via OmniRoute)`, pending: true }; + } else if (current) { + const isOmni = + current?.provider === "omniroute" || + (current?.base_url || "").includes("20128") || + (current?.base_url || "").includes("localhost"); + + if (isOmni) { + badge = { label: "OmniRoute", pending: false }; + } else { + const realProvider = current.provider || "Other"; + badge = { label: `${realProvider} (not OmniRoute)`, pending: false }; + } + } + + return ( +
+ {/* Left: role label + subtitle (now has room so long descriptions stay on one line) */} +
+
{role.label}
+
{role.description}
+
+ + {/* Right cluster: model name + status badge + actions (pushed to the right) */} +
+ {displayedModel ? ( + + {displayedModel} + + ) : ( + + )} + + {badge && ( +
+ {badge.label}{badge.pending ? " *" : ""} +
+ )} + + + + {sel && ( + + )} +
+
+ ); + })} +
+ + {/* Message (standard colored info bar like other cards) */} + {message && ( +
+ check_circle + {message} +
+ )} + + {/* Action row — primary Apply + right spacer for future actions */} +
+ + + + + {Object.keys(selections).length > 0 && ( + + {Object.keys(selections).length} role{Object.keys(selections).length === 1 ? "" : "s"} will be updated + + )} + +
+ + {/* Optional future: Reset or Manual config could go here, right-aligned */} +
+ + {/* Inline YAML preview — toggled by the Preview button, styled like other config previews on the CLI tools page */} + {previewYaml && ( +
+
+ Preview — will write to ~/.hermes/config.yaml +
+
+                {previewYaml}
+              
+
+ )} + +

+ Saves the selected models for each role into ~/.hermes/config.yaml. +

+
+ )} + + setModalRole(null)} + onSelect={(model: any) => { + if (modalRole) { + const modelValue = typeof model === "string" ? model : model?.value || model?.name; + if (modelValue) { + // Capture a useful provider label from the modal selection when available + const prov = + (model && (model.provider || model.providerId || model.group)) || "OmniRoute"; + setRoleSelection(modalRole, modelValue, prov); + } + } + setModalRole(null); + }} + showCombos={true} + activeProviders={activeProviders} + /> + + ); +} + diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx index ed2c1845f6..bae6f2571b 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/index.tsx @@ -8,3 +8,4 @@ export { default as DefaultToolCard } from "./DefaultToolCard"; export { default as AntigravityToolCard } from "./AntigravityToolCard"; export { default as CopilotToolCard } from "./CopilotToolCard"; export { default as CustomCliCard } from "./CustomCliCard"; +export { default as HermesAgentToolCard } from "./HermesAgentToolCard"; diff --git a/src/app/(dashboard)/home/ProviderQuotaWidget.tsx b/src/app/(dashboard)/home/ProviderQuotaWidget.tsx new file mode 100644 index 0000000000..26aed69024 --- /dev/null +++ b/src/app/(dashboard)/home/ProviderQuotaWidget.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTranslations } from "next-intl"; +import Card from "@/shared/components/Card"; +import ProviderIcon from "@/shared/components/ProviderIcon"; +import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers"; + +type Connection = { + id: string; + provider: string; + authType?: string; + email?: string; + name?: string; +}; + +type QuotaData = Record; + +export default function ProviderQuotaWidget() { + const t = useTranslations("usage"); + const tc = useTranslations("common"); + + const [connections, setConnections] = useState([]); + const [quotaData, setQuotaData] = useState({}); + const [loading, setLoading] = useState(true); + const [refreshingAll, setRefreshingAll] = useState(false); + + const refreshingAllRef = useRef(false); + + const fetchConnections = useCallback(async () => { + try { + const res = await fetch("/api/providers/client"); + if (!res.ok) throw new Error("Failed to load connections"); + const data = await res.json(); + return (data.connections || []) as Connection[]; + } catch { + return []; + } + }, []); + + const fetchCached = useCallback(async () => { + try { + const res = await fetch("/api/usage/provider-limits"); + if (!res.ok) throw new Error("Failed"); + const data = await res.json(); + return data.caches || {}; + } catch { + return {}; + } + }, []); + + const loadData = useCallback(async () => { + setLoading(true); + const [conns, caches] = await Promise.all([fetchConnections(), fetchCached()]); + + // Only keep connections that are usage/quota supported + const relevant = conns.filter( + (c) => + USAGE_SUPPORTED_PROVIDERS.includes(c.provider) && + (c.authType === "oauth" || c.authType === "apikey") + ); + + setConnections(relevant); + setQuotaData(caches); + setLoading(false); + }, [fetchConnections, fetchCached]); + + useEffect(() => { + loadData(); + }, [loadData]); + + const refreshAll = useCallback(async () => { + if (refreshingAllRef.current) return; + refreshingAllRef.current = true; + setRefreshingAll(true); + + try { + const res = await fetch("/api/usage/provider-limits", { method: "POST" }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || "Refresh failed"); + } + const data = await res.json(); + // Re-fetch connections + caches to get fresh state + const [conns, caches] = await Promise.all([fetchConnections(), fetchCached()]); + const relevant = conns.filter( + (c) => + USAGE_SUPPORTED_PROVIDERS.includes(c.provider) && + (c.authType === "oauth" || c.authType === "apikey") + ); + setConnections(relevant); + setQuotaData(caches || data.caches || {}); + } catch (e) { + console.error("ProviderQuotaWidget refreshAll error:", e); + } finally { + refreshingAllRef.current = false; + setRefreshingAll(false); + } + }, [fetchConnections, fetchCached]); + + // Simple summary: group by provider for display + const providerGroups = connections.reduce>((acc, conn) => { + if (!acc[conn.provider]) acc[conn.provider] = []; + acc[conn.provider].push(conn); + return acc; + }, {}); + + const providerEntries = Object.entries(providerGroups).sort(([a], [b]) => a.localeCompare(b)); + + return ( + + {/* Header with title + Refresh All in upper right */} +
+
+ account_balance +
+

{t("providerQuota") || "Provider Quota"}

+

+ {t("providerQuotaHomeHint") || "Live status across connected accounts"} +

+
+
+ + +
+ + {/* Body */} +
+ {loading ? ( +
+ progress_activity + Loading quota information... +
+ ) : providerEntries.length === 0 ? ( +
+ No quota-supported providers connected yet. +
Add accounts on the Providers page to see quota status here.
+
+ ) : ( +
+ {providerEntries.map(([provider, conns]) => { + const firstConn = conns[0]; + const cache = quotaData[firstConn?.id]; + const hasQuota = cache?.quotas && Object.keys(cache.quotas).length > 0; + + return ( +
+
+ + + {provider.charAt(0).toUpperCase() + provider.slice(1)} + + + {conns.length} account{conns.length > 1 ? "s" : ""} + +
+ + {hasQuota ? ( +
+ {Object.keys(cache.quotas).length} quota window(s) tracked +
+ ) : ( +
+ No quota data yet — click Refresh All +
+ )} + + {/* Future: embed small QuotaProgressBar for the primary window here */} +
+ ); + })} +
+ )} + + +
+
+ ); +} diff --git a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts index 164a341a37..148c94242c 100644 --- a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts +++ b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts @@ -2,9 +2,10 @@ import { NextResponse } from "next/server"; import fs from "fs/promises"; import path from "path"; import os from "os"; +import * as yaml from "js-yaml"; import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; import { getRuntimePorts } from "@/lib/runtime/ports"; -import { getOpenCodeConfigPath } from "@/shared/services/cliRuntime"; +import { getCliPrimaryConfigPath, getOpenCodeConfigPath } from "@/shared/services/cliRuntime"; import { mergeOpenCodeConfigText } from "@/shared/services/opencodeConfig"; import { guideSettingsSaveSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; @@ -16,6 +17,10 @@ import { resolveApiKey, getOrCreateApiKey } from "@/shared/services/apiKeyResolv * Save configuration for guide-based tools that have config files. * Currently supports: continue, opencode */ +export async function GET(request, { params }) { + return NextResponse.json({ error: "GET not supported for this tool" }, { status: 400 }); +} + export async function POST(request, { params }) { const authError = await requireCliToolsAuth(request); if (authError) return authError; @@ -58,6 +63,9 @@ export async function POST(request, { params }) { return await saveOpenCodeConfig({ baseUrl, apiKey, model, models, modelLabels }); case "qwen": return await saveQwenConfig({ baseUrl, apiKey, model }); + case "hermes": + return await saveHermesConfig({ baseUrl, apiKey, model }); + // hermes-agent now uses the dedicated /api/cli-tools/hermes-agent-settings endpoint default: return NextResponse.json( { error: `Direct config save not supported for: ${toolId}` }, @@ -241,3 +249,65 @@ async function saveQwenConfig({ baseUrl, apiKey, model }) { configPath, }); } + +/** + * Save Hermes config to ~/.hermes/config.yaml + * + * Hermes stores its primary routing settings in YAML. Preserve any existing + * keys, but make sure the OmniRoute provider entry is present and selected. + */ +async function saveHermesConfig({ baseUrl, apiKey, model }) { + const configPath = getCliPrimaryConfigPath("hermes") || path.join(os.homedir(), ".hermes", "config.yaml"); + const configDir = path.dirname(configPath); + + await fs.mkdir(configDir, { recursive: true }); + + const normalizedBaseUrl = String(baseUrl || "") + .trim() + .replace(/\/+$/, ""); + const providerBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`; + + if (!model) { + return NextResponse.json({ error: "model is required for Hermes" }, { status: 400 }); + } + const selectedModel = model; + + let existingConfig: Record = {}; + try { + const raw = await fs.readFile(configPath, "utf-8"); + const parsed = yaml.load(raw); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + existingConfig = parsed as Record; + } + } catch { + // No existing config or unparsable YAML — start fresh. + } + + const nextConfig = { + ...existingConfig, + model: { + ...(existingConfig.model || {}), + default: selectedModel, + provider: "omniroute", + base_url: providerBaseUrl, + }, + providers: { + ...(existingConfig.providers || {}), + omniroute: { + ...((existingConfig.providers && existingConfig.providers.omniroute) || {}), + base_url: providerBaseUrl, + api_key: apiKey || ((existingConfig.providers && existingConfig.providers.omniroute && existingConfig.providers.omniroute.api_key) || ""), + }, + }, + }; + + await fs.writeFile(configPath, yaml.dump(nextConfig, { lineWidth: -1 }), "utf-8"); + + return NextResponse.json({ + success: true, + message: `Hermes config saved to ${configPath}`, + configPath, + }); +} + + diff --git a/src/app/api/cli-tools/hermes-agent-settings/route.ts b/src/app/api/cli-tools/hermes-agent-settings/route.ts new file mode 100644 index 0000000000..f302ef05df --- /dev/null +++ b/src/app/api/cli-tools/hermes-agent-settings/route.ts @@ -0,0 +1,115 @@ +import { NextResponse } from "next/server"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; +import { getCliPrimaryConfigPath } from "@/shared/services/cliRuntime"; +import { generateHermesAgentConfig, getCurrentHermesAgentRoles } from "@/lib/cli-helper/config-generator/hermes-agent"; + +/** + * Dedicated endpoint for Hermes Agent (the advanced Nous Research terminal agent). + * This is separate from the original simple "Hermes" guided tool. + * + * GET -> returns current per-role configuration (default, delegation, auxiliary.*) + * POST -> accepts { baseUrl, keyId?, apiKey?, selections: [{role, model}, ...] } + */ + +const CONFIG_PATH = path.join(os.homedir(), ".hermes", "config.yaml"); + +function getMetadataPath(configPath: string) { + return path.join(path.dirname(configPath), ".first-setup.json"); +} + +export async function GET() { + try { + const roles = await getCurrentHermesAgentRoles(); + + const configPath = getCliPrimaryConfigPath("hermes-agent") || CONFIG_PATH; + let firstSetupAt: string | null = null; + + try { + const metaRaw = await fs.readFile(getMetadataPath(configPath), "utf8"); + const meta = JSON.parse(metaRaw); + firstSetupAt = meta.firstSetupAt || null; + } catch { + // no metadata yet + } + + return NextResponse.json({ success: true, roles, firstSetupAt }); + } catch (error) { + return NextResponse.json({ success: false, error: String(error) }, { status: 500 }); + } +} + +export async function POST(request: Request) { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + let body; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + const { baseUrl, keyId, apiKey, selections } = body; + + if (!baseUrl) { + return NextResponse.json({ error: "baseUrl is required" }, { status: 400 }); + } + + if (!Array.isArray(selections) || selections.length === 0) { + return NextResponse.json( + { error: "selections must be a non-empty array of { role, model }" }, + { status: 400 } + ); + } + + const configPath = getCliPrimaryConfigPath("hermes-agent") || CONFIG_PATH; + const configDir = path.dirname(configPath); + + await fs.mkdir(configDir, { recursive: true }); + + const payload = { + baseUrl, + keyId, + apiKey, + selections, + }; + + const result = await generateHermesAgentConfig(payload); + + if (result.error) { + return NextResponse.json({ error: result.error }, { status: 400 }); + } + + // Preview mode: return the would-be YAML without writing it (Phase 5 polish) + if (body.preview === true) { + return NextResponse.json({ + success: true, + preview: true, + yaml: result.yaml, + configPath, + }); + } + + await fs.writeFile(configPath, result.yaml, "utf-8"); + + // Record first setup time if this is the first save via OmniRoute + const metaPath = getMetadataPath(configPath); + try { + await fs.access(metaPath); + } catch { + await fs.writeFile( + metaPath, + JSON.stringify({ firstSetupAt: new Date().toISOString() }), + "utf8" + ); + } + + return NextResponse.json({ + success: true, + message: `Hermes Agent config saved to ${configPath}`, + configPath, + }); +} diff --git a/src/app/api/cli-tools/status/route.ts b/src/app/api/cli-tools/status/route.ts index 7bf7ef83e6..3277754094 100644 --- a/src/app/api/cli-tools/status/route.ts +++ b/src/app/api/cli-tools/status/route.ts @@ -47,6 +47,15 @@ async function checkToolConfigStatus(toolId: string): Promise { return "configured"; } + if (toolId === "hermes") { + const lower = content.toLowerCase(); + const hasOmniRoute = + lower.includes("omniroute") || + lower.includes(`localhost:${apiPort}`) || + lower.includes(`127.0.0.1:${apiPort}`); + return hasOmniRoute ? "configured" : "not_configured"; + } + const config = JSON.parse(content); // Each tool stores OmniRoute config differently @@ -142,7 +151,7 @@ export async function GET(request: Request) { ); // Check config status for installed+runnable tools via direct file reads - const settingsTools = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen"]; + const settingsTools = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen", "hermes"]; await Promise.all( settingsTools.map(async (toolId) => { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 89bc969435..cdc0e748b1 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1628,6 +1628,7 @@ "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", + "hermes-agent": "Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." }, "toolDescriptions": { @@ -1647,6 +1648,7 @@ "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", + "hermes-agent": "Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", "custom": "Generic OpenAI-compatible CLI or SDK configuration generator" }, "guides": { diff --git a/src/lib/cli-helper/config-generator/hermes-agent.ts b/src/lib/cli-helper/config-generator/hermes-agent.ts new file mode 100644 index 0000000000..dbed67ae8b --- /dev/null +++ b/src/lib/cli-helper/config-generator/hermes-agent.ts @@ -0,0 +1,206 @@ +/** + * Hermes Agent — Rich multi-role config generator & saver + * + * This is the implementation for the advanced "Hermes Agent" (Nous Research terminal agent). + * It is treated completely separately from the original simple "Hermes" guide tool. + * + * Hermes Agent supports many independent model slots: + * - default (main conversation) + * - delegation (sub-agent orchestrator) + * - auxiliary.* (vision, compression, web_extract, skills_hub, approval, ...) + * + * The UI (HermesAgentToolCard) will offer a dropdown for EACH of these roles. + * + * Data Model (what the frontend sends and this module consumes): + * + * interface HermesAgentRoleSelection { + * role: 'default' | 'delegation' | 'vision' | 'compression' | 'web_extract' | 'skills_hub' | 'approval' | ...; + * model: string; // the model name the user chose from OmniRoute + * } + * + * interface HermesAgentConfigPayload { + * baseUrl: string; // usually the OmniRoute base URL + * keyId?: string | null; // preferred: reference to a stored key + * apiKey?: string | null; // fallback plaintext key + * selections: HermesAgentRoleSelection[]; + * } + */ + +import path from "node:path"; +import os from "node:os"; +import * as yaml from "js-yaml"; + +export const HERMES_AGENT_ROLES = [ + { id: "default", label: "Default (main)", description: "Primary conversation model" }, + { id: "delegation", label: "Delegation (subagents)", description: "Orchestrator and sub-agent spawning model" }, + { id: "vision", label: "Vision", description: "Image and screenshot understanding" }, + { id: "compression", label: "Compression", description: "Prompt compression and summarization" }, + { id: "web_extract", label: "Web Extract", description: "Web page / content extraction" }, + { id: "skills_hub", label: "Skills Hub", description: "Skills and tool-use reasoning" }, + { id: "approval", label: "Approval", description: "Safety and approval decisions" }, +] as const; + +export type HermesAgentRole = typeof HERMES_AGENT_ROLES[number]["id"]; + +export interface HermesAgentRoleSelection { + role: HermesAgentRole; + model: string; +} + +export interface HermesAgentConfigPayload { + baseUrl: string; + keyId?: string | null; + apiKey?: string | null; + selections: HermesAgentRoleSelection[]; +} + +const CONFIG_PATH = path.join(os.homedir(), ".hermes", "config.yaml"); + +// Build a normalized base URL for Hermes (no trailing slash, no /v1 suffix on the provider entry) +function normalizeBaseUrl(base: string): string { + let b = base.trim(); + while (b.endsWith("/")) b = b.slice(0, -1); + if (b.endsWith("/v1")) b = b.slice(0, -3); + return b; +} + +function getProviderBlock(baseUrl: string, apiKey: string) { + const normalized = normalizeBaseUrl(baseUrl); + return { + provider: "omniroute", + model: "", // will be filled per-role + base_url: `${normalized}/v1`, + api_key: apiKey, + }; +} + +/** + * Generate the full merged YAML for Hermes Agent config. + * This is the core of the rich implementation. + */ +export async function generateHermesAgentConfig( + payload: HermesAgentConfigPayload +): Promise<{ yaml: string; error?: string }> { + const { baseUrl, keyId, apiKey, selections } = payload; + + if (!baseUrl) { + return { yaml: "", error: "baseUrl is required" }; + } + + // Resolve the actual key to use (in real impl we would look up keyId) + const resolvedKey = apiKey || "YOUR_OMNIROUTE_API_KEY_HERE"; + + // Read existing config if present (non-destructive merge) + let existing: any = {}; + try { + const fs = await import("node:fs/promises"); + const raw = await fs.readFile(CONFIG_PATH, "utf-8"); + existing = yaml.load(raw) || {}; + } catch { + // no existing file — start fresh + } + + // Build the providers.omniroute entry (shared) + const normalizedBase = normalizeBaseUrl(baseUrl); + const omnirouteProvider = { + base_url: `${normalizedBase}/v1`, + api_key: resolvedKey, + }; + + // Start from existing or empty + const next: any = { + ...existing, + providers: { + ...(existing.providers || {}), + omniroute: omnirouteProvider, + }, + }; + + // Apply each role selection + for (const sel of selections) { + const { role, model } = sel; + + if (role === "default") { + next.model = { + ...(existing.model || {}), + default: model, + provider: "omniroute", + base_url: `${normalizedBase}/v1`, + }; + } else if (role === "delegation") { + next.delegation = { + ...(existing.delegation || {}), + model, + provider: "omniroute", + base_url: `${normalizedBase}/v1`, + api_key: resolvedKey, + }; + } else { + // auxiliary.* roles + if (!next.auxiliary) next.auxiliary = {}; + next.auxiliary[role] = { + ...(existing.auxiliary?.[role] || {}), + provider: "omniroute", + model, + base_url: `${normalizedBase}/v1`, + api_key: resolvedKey, + }; + } + } + + const outputYaml = yaml.dump(next, { lineWidth: -1, noRefs: true }); + return { yaml: outputYaml }; +} + +/** + * Read the current Hermes Agent configuration and extract the model + * for each supported role. + */ +export async function getCurrentHermesAgentRoles(): Promise< + Record +> { + const fs = await import("node:fs/promises"); + let config: any = {}; + + try { + const raw = await fs.readFile(CONFIG_PATH, "utf-8"); + config = yaml.load(raw) || {}; + } catch { + return {}; + } + + const result: Record = {}; + + // default + if (config.model?.default) { + result.default = { + model: config.model.default, + provider: config.model.provider, + base_url: config.model.base_url, + }; + } + + // delegation + if (config.delegation?.model) { + result.delegation = { + model: config.delegation.model, + provider: config.delegation.provider, + base_url: config.delegation.base_url, + }; + } + + // auxiliary roles + if (config.auxiliary && typeof config.auxiliary === "object") { + for (const [role, val] of Object.entries(config.auxiliary)) { + if (val && typeof val === "object" && (val as any).model) { + result[role] = { + model: (val as any).model, + provider: (val as any).provider, + base_url: (val as any).base_url, + }; + } + } + } + + return result; +} diff --git a/src/lib/cli-helper/config-generator/hermes.ts b/src/lib/cli-helper/config-generator/hermes.ts new file mode 100644 index 0000000000..0fd5355d7e --- /dev/null +++ b/src/lib/cli-helper/config-generator/hermes.ts @@ -0,0 +1,47 @@ +import path from "node:path"; +import os from "node:os"; + +let yaml: typeof import("js-yaml") | null = null; +async function loadYaml() { + if (!yaml) { + yaml = await import("js-yaml"); + } + return yaml; +} + +const CONFIG_PATH = path.join(os.homedir(), ".hermes", "config.yaml"); + +export async function generateHermesConfig(options: { + baseUrl: string; + apiKey: string; + model?: string; +}): Promise { + const y = await loadYaml(); + + let base = options.baseUrl; + let end = base.length; + while (end > 0 && base[end - 1] === "/") end--; + base = end < base.length ? base.slice(0, end) : base; + if (base.endsWith("/v1")) base = base.slice(0, -3); + + if (!options.model) { + throw new Error("model is required"); + } + const model = options.model; + + const config = { + model: { + default: model, + provider: "omniroute", + base_url: `${base}/v1`, + }, + providers: { + omniroute: { + base_url: `${base}/v1`, + api_key: options.apiKey, + }, + }, + }; + + return y.dump(config, { lineWidth: -1 }); +} diff --git a/src/lib/cli-helper/config-generator/index.ts b/src/lib/cli-helper/config-generator/index.ts index beaf58bee8..59a9fb9b70 100644 --- a/src/lib/cli-helper/config-generator/index.ts +++ b/src/lib/cli-helper/config-generator/index.ts @@ -5,6 +5,8 @@ import { generateClaudeConfig } from "./claude"; import { generateClineConfig } from "./cline"; import { generateCodexConfig } from "./codex"; import { generateContinueConfig } from "./continue"; +import { generateHermesConfig } from "./hermes"; +import { generateHermesAgentConfig, type HermesAgentConfigPayload } from "./hermes-agent"; import { generateKilocodeConfig } from "./kilocode"; import { generateOpencodeConfig } from "./opencode"; @@ -21,7 +23,7 @@ export interface GenerateResult { error?: string; } -function validateBaseUrl(url: string): boolean { +export function validateBaseUrl(url: string): boolean { try { const u = new URL(url); return u.protocol === "http:" || u.protocol === "https:"; @@ -42,8 +44,12 @@ const TOOL_CONFIG_PATHS: Record = { cline: path.join(os.homedir(), ".cline", "data", "globalState.json"), kilocode: path.join(os.homedir(), ".config", "kilocode", "settings.json"), continue: path.join(os.homedir(), ".continue", "config.yaml"), + hermes: path.join(os.homedir(), ".hermes", "config.yaml"), + "hermes-agent": path.join(os.homedir(), ".hermes", "config.yaml"), }; + + type ConfigGenerator = (options: GenerateOptions) => string | Promise; const GENERATORS: Record = { @@ -53,6 +59,8 @@ const GENERATORS: Record = { cline: generateClineConfig, kilocode: generateKilocodeConfig, continue: generateContinueConfig, + hermes: generateHermesConfig, + "hermes-agent": generateHermesAgentConfig as any, // rich multi-role version }; export async function generateConfig( @@ -86,7 +94,7 @@ export async function generateConfig( } export async function generateAllConfigs(options: GenerateOptions): Promise { - const toolIds = ["claude", "codex", "opencode", "cline", "kilocode", "continue"] as const; + const toolIds = ["claude", "codex", "opencode", "cline", "kilocode", "continue", "hermes"] as const; const results = await Promise.allSettled(toolIds.map((id) => generateConfig(id, options))); return results.map((r) => diff --git a/src/lib/cli-helper/tool-detector.ts b/src/lib/cli-helper/tool-detector.ts index 7b48f46585..715b355902 100644 --- a/src/lib/cli-helper/tool-detector.ts +++ b/src/lib/cli-helper/tool-detector.ts @@ -2,8 +2,14 @@ import os from "node:os"; import path from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { getCurrentHermesAgentRoles } from "./config-generator/hermes-agent"; const execFileAsync = promisify(execFile); +let execFileImpl = execFileAsync; + +export function __setExecFileImpl(fn: typeof execFileAsync): void { + execFileImpl = fn; +} export interface DetectedTool { id: string; @@ -13,6 +19,13 @@ export interface DetectedTool { configPath: string; configured: boolean; configContents?: string; + + // Rich per-role status for Hermes Agent + hermesAgentRoles?: Record; } const TOOLS = [ @@ -22,6 +35,8 @@ const TOOLS = [ { id: "cline", name: "Cline", configPath: "~/.cline/data/globalState.json" }, { id: "kilocode", name: "Kilo Code", configPath: "~/.config/kilocode/settings.json" }, { id: "continue", name: "Continue", configPath: "~/.continue/config.yaml" }, + { id: "hermes", name: "Hermes", configPath: "~/.hermes/config.yaml" }, + { id: "hermes-agent", name: "Hermes Agent", configPath: "~/.hermes/config.yaml" }, ] as const; const BINARY_NAMES: Record = { @@ -31,6 +46,8 @@ const BINARY_NAMES: Record = { cline: "cline", kilocode: "kilocode", continue: "continue", + hermes: "hermes", + "hermes-agent": "hermes", }; function expandHome(p: string): string { @@ -50,7 +67,7 @@ function isConfigured(content: string, baseUrl: string): boolean { async function detectBinary(name: string): Promise<{ installed: boolean; version?: string }> { const binary = BINARY_NAMES[name] || name; try { - const { stdout } = await execFileAsync(binary, ["--version"], { timeout: 5000 }); + const { stdout } = await execFileImpl(binary, ["--version"], { timeout: 5000 }); const version = stdout.trim().replace(/^v/, ""); return { installed: true, version }; } catch { @@ -85,7 +102,7 @@ export async function detectTool(id: string): Promise { const configContents = await readConfigFile(tool.configPath); const configured = !!configContents && isConfigured(configContents, "http://localhost:20128"); - return { + const result: DetectedTool = { id: tool.id, name: tool.name, installed, @@ -94,6 +111,32 @@ export async function detectTool(id: string): Promise { configured, configContents: configContents ?? undefined, }; + + // Rich per-role status only for Hermes Agent + if (tool.id === "hermes-agent") { + try { + const roles = await getCurrentHermesAgentRoles(); + const richRoles: Record = {}; + + Object.entries(roles).forEach(([role, info]) => { + const usingOmni = info?.provider === "omniroute" || + (info?.base_url || "").includes("20128") || + (info?.base_url || "").includes("localhost:20128"); + + richRoles[role] = { + model: info.model, + provider: info.provider, + usingOmniRoute: usingOmni, + }; + }); + + result.hermesAgentRoles = richRoles; + } catch { + // ignore – rich status is optional + } + } + + return result; } export async function detectAllTools(): Promise { diff --git a/src/shared/components/PwaRegister.tsx b/src/shared/components/PwaRegister.tsx index a737fb196a..0e49a6070f 100644 --- a/src/shared/components/PwaRegister.tsx +++ b/src/shared/components/PwaRegister.tsx @@ -8,6 +8,11 @@ export function PwaRegister() { return; } + // Disable service worker in development to avoid chunk loading / HMR conflicts + if (process.env.NODE_ENV !== "production") { + return; + } + navigator.serviceWorker.register("/sw.js").catch(() => { // Ignore registration failures to avoid blocking app rendering. }); diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index 0ae3cb40b9..a9d49bb07c 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -329,6 +329,19 @@ export const CLI_TOOLS = { }`, }, }, + + // Rich, first-class support for the real advanced Hermes Agent (Nous Research) + // Separate from the original simple "Hermes" guide above. + "hermes-agent": { + id: "hermes-agent", + name: "Hermes Agent", + icon: "terminal", + color: "#8B5CF6", + description: "Hermes Agent (by Nousresearch) — advanced multi-role terminal AI", + docsUrl: "/docs?section=cli-tools&tool=hermes-agent", + configType: "custom", + defaultCommand: "hermes", + }, amp: { id: "amp", name: "Amp CLI", diff --git a/src/shared/constants/homeWidgets.ts b/src/shared/constants/homeWidgets.ts new file mode 100644 index 0000000000..f12599bcd8 --- /dev/null +++ b/src/shared/constants/homeWidgets.ts @@ -0,0 +1,5 @@ +/** + * Settings keys for widgets that can be pinned / shown on the Home page. + */ + +export const PIN_PROVIDER_QUOTA_TO_HOME_KEY = "pinProviderQuotaToHome"; diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index 7ed2a88b4d..fd03977e0f 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -131,6 +131,7 @@ const CLI_TOOLS: Record = { }, }, hermes: { + // Original / legacy simple Hermes entry (recovered from origin/main) defaultCommand: "hermes", envBinKey: "CLI_HERMES_BIN", requiresBinary: false, @@ -139,6 +140,16 @@ const CLI_TOOLS: Record = { config: ".config/hermes/config.json", }, }, + "hermes-agent": { + // Rich first-class support for the advanced Hermes Agent (multi-role: default, delegation, auxiliary.*) + defaultCommand: "hermes", + envBinKey: "CLI_HERMES_BIN", + requiresBinary: true, + healthcheckTimeoutMs: 4000, + paths: { + config: ".hermes/config.yaml", + }, + }, amp: { defaultCommand: "amp", envBinKey: "CLI_AMP_BIN", diff --git a/tests/unit/cli-helper/config-generator.test.ts b/tests/unit/cli-helper/config-generator.test.ts index b242424ec8..cc8e90cbeb 100644 --- a/tests/unit/cli-helper/config-generator.test.ts +++ b/tests/unit/cli-helper/config-generator.test.ts @@ -50,6 +50,18 @@ describe("config-generator", () => { assert.ok("configPath" in result); }); + it("returns success for valid hermes config", async () => { + const result = await generator.generateConfig("hermes", { + baseUrl: "http://localhost:20128", + apiKey: "sk-test", + model: "gpt-5.4-mini", + }); + assert.strictEqual(result.success, true); + assert.ok(result.configPath.endsWith(".hermes/config.yaml")); + assert.ok(String(result.content || "").includes("providers:")); + assert.ok(String(result.content || "").includes("omniroute")); + }); + it("returns error for unknown tool", async () => { const result = await generator.generateConfig("unknown-tool-xyz", { baseUrl: "http://localhost:20128", @@ -67,7 +79,103 @@ describe("config-generator", () => { apiKey: "sk-xxx", }); assert.ok(Array.isArray(results)); - assert.strictEqual(results.length, 6); // claude, codex, opencode, cline, kilocode, continue + assert.strictEqual(results.length, 7); // claude, codex, opencode, cline, kilocode, continue, hermes + }); + }); + + describe("hermes-agent (rich multi-role)", () => { + it("exports HERMES_AGENT_ROLES with expected roles", async () => { + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + assert.ok(Array.isArray(hermesAgent.HERMES_AGENT_ROLES)); + const ids = hermesAgent.HERMES_AGENT_ROLES.map((r: any) => r.id); + assert.ok(ids.includes("default")); + assert.ok(ids.includes("delegation")); + assert.ok(ids.includes("vision")); + assert.ok(ids.includes("approval")); + }); + + it("getCurrentHermesAgentRoles returns an object", async () => { + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + const roles = await hermesAgent.getCurrentHermesAgentRoles(); + assert.ok(typeof roles === "object" && roles !== null); + }); + + it("generateHermesAgentConfig returns yaml string for valid payload", async () => { + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + const result = await hermesAgent.generateHermesAgentConfig({ + baseUrl: "http://localhost:20128", + apiKey: "sk-test-omniroute", + selections: [ + { role: "default", model: "gpt-4o" }, + { role: "delegation", model: "claude-3-5-sonnet" }, + { role: "vision", model: "gpt-4o" }, + ], + }); + + assert.ok(!result.error); + assert.ok(typeof result.yaml === "string"); + assert.ok(result.yaml.length > 50); + assert.ok(result.yaml.includes("provider: omniroute")); + }); + + it("generateHermesAgentConfig includes auxiliary section for non-default roles", async () => { + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + const result = await hermesAgent.generateHermesAgentConfig({ + baseUrl: "http://localhost:20128", + apiKey: "sk-test", + selections: [ + { role: "compression", model: "test-model" }, + { role: "skills_hub", model: "test-model-2" }, + ], + }); + + assert.ok(result.yaml.includes("auxiliary:")); + assert.ok(result.yaml.includes("compression:")); + }); + + it("generateHermesAgentConfig returns error when baseUrl is missing", async () => { + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + const result = await hermesAgent.generateHermesAgentConfig({ + baseUrl: "", + selections: [{ role: "default", model: "x" }], + } as any); + + assert.ok(result.error); + assert.ok(result.error.includes("baseUrl")); + }); + + it("generateHermesAgentConfig correctly structures delegation and auxiliary roles", async () => { + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + const result = await hermesAgent.generateHermesAgentConfig({ + baseUrl: "http://localhost:20128", + apiKey: "sk-test", + selections: [ + { role: "default", model: "model-default" }, + { role: "delegation", model: "model-delegation" }, + { role: "approval", model: "model-approval" }, + ], + }); + + const yaml = result.yaml; + assert.ok(yaml.includes("model:")); + assert.ok(yaml.includes("default: model-default")); + assert.ok(yaml.includes("delegation:")); + assert.ok(yaml.includes("auxiliary:")); + assert.ok(yaml.includes("approval:")); + }); + + it("generateHermesAgentConfig performs non-destructive merge (preserves other keys)", async () => { + // This test mainly verifies the function doesn't blow away unrelated config + const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts"); + const result = await hermesAgent.generateHermesAgentConfig({ + baseUrl: "http://localhost:20128", + apiKey: "sk-test", + selections: [{ role: "default", model: "new-model" }], + }); + + // Should still contain providers block and the new model + assert.ok(result.yaml.includes("providers:")); + assert.ok(result.yaml.includes("new-model")); }); }); }); diff --git a/tests/unit/cli-helper/tool-detector.test.ts b/tests/unit/cli-helper/tool-detector.test.ts index 2c0aaffa49..fe68245851 100644 --- a/tests/unit/cli-helper/tool-detector.test.ts +++ b/tests/unit/cli-helper/tool-detector.test.ts @@ -10,6 +10,9 @@ describe("tool-detector", () => { if (cmd === "opencode") { return { stdout: "v1.0.0\n" }; } + if (cmd === "hermes") { + return { stdout: "v0.75.3\n" }; + } if (cmd === "which") { return { stdout: "/usr/local/bin/opencode\n" }; } @@ -33,6 +36,18 @@ describe("tool-detector", () => { assert.ok(result!.configPath.includes(".config/opencode")); assert.strictEqual(typeof result!.configured, "boolean"); }); + + it("returns DetectedTool object for Hermes with the Hermes config path", async () => { + const result = await toolDetector.detectTool("hermes"); + assert.ok(result !== null); + assert.strictEqual(result!.id, "hermes"); + assert.strictEqual(result!.name, "Hermes"); + assert.strictEqual(result!.installed, true); + assert.strictEqual(result!.version, "0.75.3"); + assert.ok(result!.configPath.includes(".hermes/config.yaml")); + assert.strictEqual(typeof result!.configured, "boolean"); + }); + }); describe("detectAllTools", () => { diff --git a/tests/unit/guide-settings-route.test.ts b/tests/unit/guide-settings-route.test.ts index 31e385401f..0be7117277 100644 --- a/tests/unit/guide-settings-route.test.ts +++ b/tests/unit/guide-settings-route.test.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; import { SignJWT } from "jose"; import { parse } from "jsonc-parser"; +import * as yaml from "js-yaml"; const guideSettingsRoute = await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts"); @@ -12,6 +13,7 @@ const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-qwen-test-" + Date.now()); const QWEN_CONFIG_PATH = path.join(DUMMY_HOME, ".qwen", "settings.json"); const QWEN_ENV_PATH = path.join(DUMMY_HOME, ".qwen", ".env"); const OPENCODE_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "opencode", "opencode.json"); +const HERMES_CONFIG_PATH = path.join(DUMMY_HOME, ".hermes", "config.yaml"); const originalXDG = process.env.XDG_CONFIG_HOME; const originalAppData = process.env.APPDATA; const originalJwtSecret = process.env.JWT_SECRET; @@ -87,6 +89,27 @@ test("guide-settings POST creates new qwen settings.json if it doesn't exist", a assert.equal(content.model?.name, "gemini-cli/gemini-3.1-pro-preview"); }); +test("guide-settings POST creates new hermes config.yaml if it doesn't exist", async () => { + const req = await buildRequest({ + baseUrl: "http://my-omni", + apiKey: "sk-hermes", + model: "gpt-5.4-mini", + }); + const response = (await guideSettingsRoute.POST(req, { params: { toolId: "hermes" } })) as Response; + const data = (await response.json()) as any; + + assert.equal(response.status, 200, "Response should be OK"); + assert.equal(data.success, true); + + const content = yaml.load(await fs.readFile(HERMES_CONFIG_PATH, "utf-8")) as any; + assert.equal(content.model?.default, "gpt-5.4-mini"); + assert.equal(content.model?.provider, "omniroute"); + assert.equal(content.model?.base_url, "http://my-omni/v1"); + assert.equal(content.providers?.omniroute?.base_url, "http://my-omni/v1"); + assert.ok(String(content.providers?.omniroute?.api_key || "").startsWith("sk-")); +}); + + test("guide-settings POST merges into existing qwen settings.json", async () => { await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true }); await fs.writeFile( From 85c1d02b3f7a95a89895b74568ea423f7345f688 Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Fri, 22 May 2026 04:24:17 +0700 Subject: [PATCH 047/112] feat: cloud agents UX, skills fixes, memory stats, docs packaging (#2516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: cloud agents UX, skills fixes, memory stats, docs packaging — integrated into release/v3.8.2 --- package.json | 3 + .../dashboard/cloud-agents/page.tsx | 961 ++++++++++++------ src/app/(dashboard)/dashboard/skills/page.tsx | 35 + src/app/api/memory/route.ts | 19 + src/app/api/v1/agents/credentials/route.ts | 84 ++ src/app/api/v1/agents/health/route.ts | 92 ++ src/lib/cloudAgent/credentials.ts | 104 ++ src/lib/memory/settings.ts | 2 +- src/lib/skills/providerSettings.ts | 2 +- .../unit/cloud-agent-credentials-api.test.ts | 117 +++ tests/unit/cloud-agent-health-api.test.ts | 74 ++ tests/unit/memory-settings-default.test.ts | 25 + tests/unit/memory-stats-api.test.ts | 93 ++ tests/unit/provider-settings-default.test.ts | 42 + 14 files changed, 1351 insertions(+), 302 deletions(-) create mode 100644 src/app/api/v1/agents/credentials/route.ts create mode 100644 src/app/api/v1/agents/health/route.ts create mode 100644 src/lib/cloudAgent/credentials.ts create mode 100644 tests/unit/cloud-agent-credentials-api.test.ts create mode 100644 tests/unit/cloud-agent-health-api.test.ts create mode 100644 tests/unit/memory-settings-default.test.ts create mode 100644 tests/unit/memory-stats-api.test.ts create mode 100644 tests/unit/provider-settings-default.test.ts diff --git a/package.json b/package.json index 7ef7bc48d2..59d40db35e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "scripts/dev/sync-env.mjs", "scripts/build/native-binary-compat.mjs", "scripts/build/build-next-isolated.mjs", + "docs/", + "!docs/i18n/", + "!docs/diagrams/", "README.md", "LICENSE" ], diff --git a/src/app/(dashboard)/dashboard/cloud-agents/page.tsx b/src/app/(dashboard)/dashboard/cloud-agents/page.tsx index 1b7e4dc004..ee68c9d9a9 100644 --- a/src/app/(dashboard)/dashboard/cloud-agents/page.tsx +++ b/src/app/(dashboard)/dashboard/cloud-agents/page.tsx @@ -4,6 +4,8 @@ import { useState, useEffect, useCallback } from "react"; import { Card, Button, Input, Badge } from "@/shared/components"; import { useTranslations } from "next-intl"; +// ── Types ──────────────────────────────────────────────────────────────────── + interface CloudAgentTask { id: string; providerId: string; @@ -26,34 +28,75 @@ interface CloudAgentTask { }>; } +type TabId = "tasks" | "agents" | "settings"; +type TaskStatus = CloudAgentTask["status"]; + +// ── Constants ──────────────────────────────────────────────────────────────── + const CLOUD_AGENTS = [ { id: "jules", name: "Jules", provider: "Google", description: "Google's autonomous coding agent", - icon: "🟡", - color: "bg-yellow-500/10 text-yellow-600", + icon: "smart_toy", + iconBg: "bg-yellow-500/10", + iconColor: "text-yellow-600", }, { id: "devin", name: "Devin", provider: "Cognition", description: "Cognition's AI software engineer", - icon: "🔵", - color: "bg-blue-500/10 text-blue-600", + icon: "psychology", + iconBg: "bg-blue-500/10", + iconColor: "text-blue-600", }, { id: "codex-cloud", name: "Codex Cloud", provider: "OpenAI", description: "OpenAI's cloud-based coding agent", - icon: "⚡", - color: "bg-emerald-500/10 text-emerald-600", + icon: "cloud", + iconBg: "bg-emerald-500/10", + iconColor: "text-emerald-600", }, ]; +const STATUS_OPTIONS: { value: TaskStatus | "all"; labelKey: string }[] = [ + { value: "all", labelKey: "filterAll" }, + { value: "queued", labelKey: "statusPending" }, + { value: "running", labelKey: "statusRunning" }, + { value: "awaiting_approval", labelKey: "statusWaitingApproval" }, + { value: "completed", labelKey: "statusCompleted" }, + { value: "failed", labelKey: "statusFailed" }, + { value: "cancelled", labelKey: "statusCancelled" }, +]; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function getAgentInfo(providerId: string) { + return CLOUD_AGENTS.find((a) => a.id === providerId) || CLOUD_AGENTS[0]; +} + +function formatDuration(start: string, end: string) { + const ms = new Date(end).getTime() - new Date(start).getTime(); + if (ms < 1000) return `${ms}ms`; + const secs = Math.floor(ms / 1000); + if (secs < 60) return `${secs}s`; + const mins = Math.floor(secs / 60); + const remainSecs = secs % 60; + return `${mins}m ${remainSecs}s`; +} + +// ── Component ──────────────────────────────────────────────────────────────── + export default function CloudAgentsPage() { + const [activeTab, setActiveTab] = useState("tasks"); + const t = useTranslations("cloudAgents"); + + // ── Tasks state ────────────────────────────────────────────────────────── + const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); @@ -67,14 +110,48 @@ export default function CloudAgentsPage() { autoCreatePr: true, }); const [messageInput, setMessageInput] = useState(""); - const t = useTranslations("cloudAgents"); + const [statusFilter, setStatusFilter] = useState("all"); + const [providerFilter, setProviderFilter] = useState("all"); + + // ── Agents health state ────────────────────────────────────────────────── + + const [agentHealth, setAgentHealth] = useState>({}); + + // ── Settings state (localStorage) ──────────────────────────────────────── + + const [settings, setSettings] = useState({ + autoCreatePr: true, + requireApproval: false, + enabled: true, + }); + + // ── Load settings from localStorage ────────────────────────────────────── + + useEffect(() => { + try { + const stored = localStorage.getItem("omniroute-cloud-agents-settings"); + if (stored) setSettings(JSON.parse(stored)); + } catch { + // ignore + } + }, []); + + const updateSetting = (key: keyof typeof settings, value: boolean) => { + const next = { ...settings, [key]: value }; + setSettings(next); + try { + localStorage.setItem("omniroute-cloud-agents-settings", JSON.stringify(next)); + } catch { + // ignore + } + }; + + // ── Task helpers ───────────────────────────────────────────────────────── const upsertTask = useCallback((task: CloudAgentTask) => { setTasks((prev) => { - const exists = prev.some((current) => current.id === task.id); - return exists - ? prev.map((current) => (current.id === task.id ? task : current)) - : [task, ...prev]; + const exists = prev.some((c) => c.id === task.id); + return exists ? prev.map((c) => (c.id === task.id ? task : c)) : [task, ...prev]; }); setSelectedTask((current) => (current?.id === task.id ? task : current)); }, []); @@ -97,6 +174,48 @@ export default function CloudAgentsPage() { fetchTasks(); }, [fetchTasks]); + // ── Auto-poll when tasks are running/queued ────────────────────────────── + + const hasActiveTasks = tasks.some((t) => t.status === "running" || t.status === "queued"); + + useEffect(() => { + if (!hasActiveTasks) return; + const id = setInterval(() => { + fetchTasks(); + }, 5000); + return () => clearInterval(id); + }, [hasActiveTasks, fetchTasks]); + + // ── Fetch agent health ─────────────────────────────────────────────────── + + const fetchAgentHealth = useCallback(async () => { + try { + const res = await fetch("/api/v1/agents/health"); + if (res.ok) { + const data = await res.json(); + if (data.data) setAgentHealth(data.data); + } + } catch (err) { + console.error("Failed to fetch agent health:", err); + } + }, []); + + // ── Tab mount effects ──────────────────────────────────────────────────── + + useEffect(() => { + if (activeTab === "agents") fetchAgentHealth(); + }, [activeTab, fetchAgentHealth]); + + // ── Filtered tasks ─────────────────────────────────────────────────────── + + const filteredTasks = tasks.filter((task) => { + if (statusFilter !== "all" && task.status !== statusFilter) return false; + if (providerFilter !== "all" && task.providerId !== providerFilter) return false; + return true; + }); + + // ── Task actions (preserved from original) ─────────────────────────────── + const handleCreateTask = async (e: React.FormEvent) => { e.preventDefault(); setCreating(true); @@ -113,9 +232,7 @@ export default function CloudAgentsPage() { providerId: newTask.providerId, prompt: newTask.prompt, source, - options: { - autoCreatePr: newTask.autoCreatePr, - }, + options: { autoCreatePr: newTask.autoCreatePr }, }), }); if (res.ok) { @@ -143,10 +260,7 @@ export default function CloudAgentsPage() { const res = await fetch(`/api/v1/agents/tasks/${selectedTask.id}`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - action: "message", - message: messageInput, - }), + body: JSON.stringify({ action: "message", message: messageInput }), }); if (res.ok) { const data = await res.json(); @@ -198,44 +312,37 @@ export default function CloudAgentsPage() { }); if (res.ok) { setTasks((prev) => prev.filter((t) => t.id !== taskId)); - if (selectedTask?.id === taskId) { - setSelectedTask(null); - } + if (selectedTask?.id === taskId) setSelectedTask(null); } } catch (err) { console.error("Failed to delete task:", err); } }; + // ── Render helpers ─────────────────────────────────────────────────────── + const getStatusBadge = (status: string) => { - const statusMap: Record = { - queued: { color: "bg-zinc-500/10 text-zinc-500", label: t("statusPending") }, - running: { color: "bg-blue-500/10 text-blue-500", label: t("statusRunning") }, - awaiting_approval: { - color: "bg-amber-500/10 text-amber-600", - label: t("statusWaitingApproval"), - }, - completed: { color: "bg-emerald-500/10 text-emerald-600", label: t("statusCompleted") }, - failed: { color: "bg-red-500/10 text-red-500", label: t("statusFailed") }, - cancelled: { color: "bg-zinc-500/10 text-zinc-400", label: t("statusCancelled") }, + const statusMap: Record< + string, + { variant: "default" | "primary" | "success" | "warning" | "error" | "info"; label: string } + > = { + queued: { variant: "default", label: t("statusPending") }, + running: { variant: "info", label: t("statusRunning") }, + awaiting_approval: { variant: "warning", label: t("statusWaitingApproval") }, + completed: { variant: "success", label: t("statusCompleted") }, + failed: { variant: "error", label: t("statusFailed") }, + cancelled: { variant: "default", label: t("statusCancelled") }, }; const s = statusMap[status] || statusMap.queued; return ( - - {status === "running" && } + {s.label} - + ); }; - const getAgentInfo = (providerId: string) => { - return CLOUD_AGENTS.find((a) => a.id === providerId) || CLOUD_AGENTS[0]; - }; - const getPlanText = (task: CloudAgentTask) => { - return task.activities.find((activity) => activity.type === "plan")?.content || ""; + return task.activities.find((a) => a.type === "plan")?.content || ""; }; const formatResult = (result: CloudAgentTask["result"]) => { @@ -244,6 +351,16 @@ export default function CloudAgentsPage() { return JSON.stringify(result, null, 2); }; + // ── Tab definitions ────────────────────────────────────────────────────── + + const tabs: { id: TabId; label: string; icon: string }[] = [ + { id: "tasks", label: t("tasksTab") || "Tasks", icon: "task_alt" }, + { id: "agents", label: t("agentsTab") || "Agents", icon: "smart_toy" }, + { id: "settings", label: t("settingsTab") || "Settings", icon: "tune" }, + ]; + + // ── Loading state ──────────────────────────────────────────────────────── + if (loading) { return (
@@ -253,299 +370,543 @@ export default function CloudAgentsPage() { ); } + // ── Main render ────────────────────────────────────────────────────────── + return (
+ {/* Header */} -
-
-
-

{t("aboutTitle")}

-

{t("aboutDescription")}

-
+
+
+

{t("aboutTitle")}

+

{t("aboutDescription")}

-
- {CLOUD_AGENTS.map((agent) => ( -
-
- {agent.icon} -

{agent.name}

-
-

{agent.description}

-

{agent.provider}

+
+ + + {settings.enabled + ? t("agentsEnabled") || "Enabled" + : t("agentsDisabled") || "Disabled"} + +
+
+ + + {/* Tab bar */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* ── Tasks Tab ──────────────────────────────────────────────────────── */} + {activeTab === "tasks" && ( +
+ {/* Create task form */} + +
+
+ add_task +
+
+

{t("newTaskTitle")}

+

{t("newTaskDescription")}

- ))} -
-
- {t("howItWorksTitle")} - {t("howItWorksDesc")} -
-
- - - -
-
- add_task -
-
-

{t("newTaskTitle")}

-

{t("newTaskDescription")}

-
-
-
-
-
- -
-
-
- -