From 142ae93498fd5bf90a5eb6fdc3c2892477630635 Mon Sep 17 00:00:00 2001 From: excessivechaos Date: Mon, 17 Aug 2026 07:19:26 -0700 Subject: [PATCH 001/135] fix(network): bound direct-path response-start timeout --- .env.example | 8 + ...irect-dispatcher-response-start-timeout.md | 1 + docs/reference/ENVIRONMENT.md | 1 + open-sse/utils/directResponseStartTimeout.ts | 77 ++++++++++ open-sse/utils/proxyFetch.ts | 75 +++++---- ...irect-response-start-timeout-10214.test.ts | 143 ++++++++++++++++++ 6 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md create mode 100644 open-sse/utils/directResponseStartTimeout.ts create mode 100644 tests/unit/proxyfetch-direct-response-start-timeout-10214.test.ts diff --git a/.env.example b/.env.example index 970bb9f3cf..2670350880 100644 --- a/.env.example +++ b/.env.example @@ -1283,6 +1283,14 @@ CURSOR_USER_AGENT="Cursor/3.4" # FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body # FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s) # FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s) +# OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS=30000 # Bounded response-start window per direct +# # (no-proxy) attempt (#10214). A silently-dropped +# # pooled keep-alive socket surfaces no transport +# # error, so without this bound a direct request can +# # stall until undici's headersTimeout (600s) or the +# # caller's deadline; on expiry the request retries +# # once on a fresh no-keep-alive socket. 0 disables +# # the bound (default: 30000 = 30s). # Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the # fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min). diff --git a/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md b/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md new file mode 100644 index 0000000000..9354b02822 --- /dev/null +++ b/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md @@ -0,0 +1 @@ +- **fix(network):** direct (no-proxy) egress now bounds each attempt's response-start window (default 30s, `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS`) and retries once on a fresh no-keep-alive socket, so a silently-dropped pooled keep-alive connection can no longer stall direct providers (opencode-go, command-code) until a service restart ([#10214](https://github.com/diegosouzapw/OmniRoute/issues/10214)) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index a47db4ed0c..2190a8e06e 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -698,6 +698,7 @@ REQUEST_TIMEOUT_MS (global override) | `OMNIROUTE_AGENT_GOAL_STREAM_RECOVERY` | `true` | Enable early stream recovery automatically for detected `/goal` agent runs. Set `false`/`0`/`off` to disable the goal-specific opt-in. This can only ADD recovery on top of the operator default — it never overrides an explicit `STREAM_RECOVERY_ENABLED`/DB settings opt-out. | | `OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS` | _(off)_ | Strip non-standard `codex.*` SSE events (e.g. `codex.rate_limits`) that break the OpenAI SDK's `responses.stream()` with a 502. Set `true`/`1`/`yes` to enable. | | `FETCH_HEADERS_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive response headers. | +| `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS` | `30000` (30s) | Maximum response-start wait (ms) for each direct no-proxy attempt. A timeout retries once on a fresh socket; set `0` to disable the bound and retain the previous behavior. | | `FETCH_BODY_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive the full response body. | | `FETCH_CONNECT_TIMEOUT_MS` | `30000` | TCP connection establishment timeout. | | `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive socket idle timeout. | diff --git a/open-sse/utils/directResponseStartTimeout.ts b/open-sse/utils/directResponseStartTimeout.ts new file mode 100644 index 0000000000..90e7b6a04a --- /dev/null +++ b/open-sse/utils/directResponseStartTimeout.ts @@ -0,0 +1,77 @@ +type DirectFetchOptions = RequestInit & { dispatcher?: unknown }; +type DirectFetch = ( + input: RequestInfo | URL, + options: DirectFetchOptions +) => Promise; + +const DEFAULT_DIRECT_HEADERS_TIMEOUT_MS = 30_000; +const DIRECT_RESPONSE_START_TIMEOUT_CODE = "DIRECT_RESPONSE_START_TIMEOUT"; + +export function resolveDirectHeadersTimeoutMs( + env: Record = process.env +): number { + const raw = env.OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS; + if (raw == null || raw.trim() === "") return DEFAULT_DIRECT_HEADERS_TIMEOUT_MS; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0; +} + +function createDirectResponseStartTimeout(timeoutMs: number): Error & { code: string } { + const err = new Error( + `Direct response did not start within ${timeoutMs}ms — retrying on a fresh socket` + ) as Error & { code: string }; + err.name = "TimeoutError"; + err.code = DIRECT_RESPONSE_START_TIMEOUT_CODE; + return err; +} + +export function isDirectResponseStartTimeout(err: unknown): boolean { + return ( + !!err && + typeof err === "object" && + "code" in err && + err.code === DIRECT_RESPONSE_START_TIMEOUT_CODE + ); +} + +function mergeAbortSignals( + primary: AbortSignal | null | undefined, + secondary: AbortSignal +): AbortSignal { + if (!primary) return secondary; + if (primary.aborted) return primary; + const controller = new AbortController(); + const onPrimaryAbort = () => controller.abort(primary.reason); + const onSecondaryAbort = () => controller.abort(secondary.reason); + const cleanup = () => { + primary.removeEventListener("abort", onPrimaryAbort); + secondary.removeEventListener("abort", onSecondaryAbort); + }; + primary.addEventListener("abort", onPrimaryAbort, { once: true }); + secondary.addEventListener("abort", onSecondaryAbort, { once: true }); + controller.signal.addEventListener("abort", cleanup, { once: true }); + return controller.signal; +} + +export async function directFetchWithBoundedResponseStart( + input: RequestInfo | URL, + options: DirectFetchOptions, + fetchImpl: DirectFetch, + timeoutMs: number +): Promise { + if (!timeoutMs || timeoutMs <= 0) return fetchImpl(input, options); + const attemptController = new AbortController(); + const timer = setTimeout( + () => attemptController.abort(createDirectResponseStartTimeout(timeoutMs)), + timeoutMs + ); + timer.unref?.(); + try { + return await fetchImpl(input, { + ...options, + signal: mergeAbortSignals(options.signal, attemptController.signal), + }); + } finally { + clearTimeout(timer); + } +} diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index 56920ccef5..e8e3ea26f8 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -19,6 +19,11 @@ import { isControlPlaneProxyDirectFallbackEnabled, isFeatureFlagEnabled, } from "@/shared/utils/featureFlags"; +import { + directFetchWithBoundedResponseStart, + isDirectResponseStartTimeout, + resolveDirectHeadersTimeoutMs, +} from "./directResponseStartTimeout.ts"; // #9100: relay egress (Vercel / Deno / Cloudflare edge functions) used to go // through bare `originalFetch` — NO connection pooling, NO timeout, NO retry. @@ -154,7 +159,6 @@ type TlsFingerprintStore = { provider?: string | null; sessionScope?: string; }; - /** * #5217 (Gap-secondary): a mutable sink that records the proxy actually applied * by `runWithProxyContext` for the in-flight request. Executors that pin their @@ -802,15 +806,7 @@ async function patchedFetch( (deps.nativeFetch as FetchWithDispatcher | undefined) ?? originalFetchWithDispatcher; return _nativeFetch(input, options); } - // Direct connection (no proxy) — use undici with custom dispatcher for timeout control. - // Falls back to original native fetch if dispatcher initialization fails (#1054). - // Retries once on transient dispatcher errors before falling back (fix: proxyfetch-undici-retry). - // - // Non-replayable body guard: if the body is stream-like (ReadableStream/Blob) - // or the input is a Request that carries a body, the first dispatcher attempt - // owns that body. Retrying or falling back to native fetch would replay a - // consumed/locked body and can mask the original transport error with - // "Response body object should not be disturbed or locked". + // Direct undici path: bound response-start, fresh-socket retry, and body guard. const hasNonReplayableBody = requestHasNonReplayableBody(input, options); const maxAttempts = hasNonReplayableBody ? 1 : 2; const _undiciDirect = @@ -818,32 +814,44 @@ async function patchedFetch( const _nativeFallback = (deps.nativeFetch as FetchWithDispatcher | undefined) ?? originalFetchWithDispatcher; let lastDispatcherError: unknown = null; + const directHeadersTimeoutMs = resolveDirectHeadersTimeoutMs(); + let targetHostForLogs = ""; + try { + targetHostForLogs = new URL(targetUrl).host; + } catch { + // ignore — logging is best-effort + } for (let attempt = 0; attempt < maxAttempts; attempt++) { try { - return await _undiciDirect(input, { - ...options, - // #4252: first attempt uses the pooled keep-alive dispatcher; a retry - // (after a transient socket error) uses the no-keep-alive dispatcher so - // it opens a FRESH socket instead of grabbing another stale pooled one - // — the burst pattern was the retry re-hitting a dead pooled socket and - // then falling through to native fetch (which also pools) → 502. - dispatcher: attempt === 0 ? getDefaultDispatcher() : getRetryDispatcher(), - }); + return await directFetchWithBoundedResponseStart( + input, + { + ...options, + dispatcher: attempt === 0 ? getDefaultDispatcher() : getRetryDispatcher(), + }, + _undiciDirect, + directHeadersTimeoutMs + ); } catch (dispatcherError) { + if (isDirectResponseStartTimeout(dispatcherError)) { + if (attempt === 0 && maxAttempts > 1) { + console.warn( + `[ProxyFetch] Direct response-start timeout (${directHeadersTimeoutMs}ms) on pooled dispatcher — retrying on fresh no-keep-alive dispatcher: ${targetHostForLogs}` + ); + lastDispatcherError = dispatcherError; + continue; + } + throw dispatcherError; + } const msg = dispatcherError instanceof Error ? dispatcherError.message : String(dispatcherError); - // CAUTION: Do NOT fallback to native fetch if the error is a version mismatch (invalid onRequestStart) - // because the native fetch will definitely fail with the undici v8 dispatcher. if (msg.includes("onRequestStart")) { console.error( `[ProxyFetch] Fatal version mismatch: Dispatcher (v8) vs Fetch (v6/native). Hardware upgrade or SOCKS5 config isolation required. Error: ${msg}` ); throw dispatcherError; } - // Only retry/fallback for connection/dispatcher errors, not HTTP errors. - // Prefer the .code property when available (more stable across undici - // versions than message-string matching); fall back to substring match - // for errors that lack a structured code. + // Retry/fallback only for connection errors, never HTTP errors. tagProxyUnreachable(dispatcherError); const errCode = (dispatcherError as { code?: unknown })?.code; if ( @@ -854,10 +862,7 @@ async function patchedFetch( msg.includes("UND_ERR") ) { if (attempt === 0 && maxAttempts > 1) { - // First failure — retry once after a short backoff before giving up. - // Delay is OMNIROUTE_RETRY_BACKOFF_MS (default 10ms): a fixed backoff - // beats random jitter here because the retry opens a fresh socket, so - // jitter was pure added latency with no herd benefit. + // Retry after a short fixed backoff on a fresh socket. lastDispatcherError = dispatcherError; await new Promise((r) => setTimeout(r, RETRY_BACKOFF_MS)); continue; @@ -873,7 +878,7 @@ async function patchedFetch( throw tagProxyUnreachable(dispatcherError); } - // All attempts exhausted — try proxy fallback before native fetch + // Exhausted attempts: try proxy fallback before native fetch. if ( !tlsDirectFallback && source === "direct" && @@ -899,20 +904,14 @@ async function patchedFetch( } } } - // Preserve original phrase intact for monitoring: "Undici dispatcher failed, falling back to native fetch" - // #4252: append the flattened err.cause (code/syscall/errno/address) — the bare - // "fetch failed" message hides what actually broke, making bursts undiagnosable. + // Preserve the original monitoring phrase and append the transport cause. console.warn( `[ProxyFetch] Undici dispatcher failed, falling back to native fetch (after retry): ${describeFetchCause(dispatcherError)}` ); try { return await _nativeFallback(input, options); } catch (nativeError) { - // #4252: both the undici dispatcher AND native fetch failed. Surface BOTH - // causes (server log) and tag the propagated error so the combo executor sees - // a diagnosable failure IMMEDIATELY instead of a bare "fetch failed" — the - // latter left jobs sitting until the 30s semaphore queue timeout, which then - // tripped the circuit breaker. + // Surface both dispatcher and native causes immediately. const detail = `dispatcher=[${describeFetchCause(dispatcherError)}] native=[${describeFetchCause(nativeError)}]`; console.warn(`[ProxyFetch] native fetch fallback ALSO failed: ${detail}`); if (nativeError instanceof Error) { diff --git a/tests/unit/proxyfetch-direct-response-start-timeout-10214.test.ts b/tests/unit/proxyfetch-direct-response-start-timeout-10214.test.ts new file mode 100644 index 0000000000..fa3a789612 --- /dev/null +++ b/tests/unit/proxyfetch-direct-response-start-timeout-10214.test.ts @@ -0,0 +1,143 @@ +/** + * #10214 — Direct (no-proxy) requests stall on a silently-dropped pooled + * keep-alive socket until the caller's deadline or a service restart. + * + * The default direct dispatcher pools keep-alive sockets for up to + * `fetchKeepAliveTimeoutMs` (4 s). A socket that silently drops (half-open, no + * RST) surfaces NO transport error — undici's headersTimeout (600 s default) is + * the only guard, so the existing fresh-socket retry (which fires on + * UND_ERR/ECONNRESET/fetch-failed) never triggers. Observed live: opencode-go + * and command-code stall 100% of routed requests until `systemctl restart`. + * + * The fix bounds the response-start window per direct attempt + * (`OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS`, default 30 s) and retries once on the + * fresh no-keep-alive dispatcher (a brand-new socket) when the pooled attempt + * times out — converting the zombie-socket stall into a clean failover. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { proxyFetch } from "../../open-sse/utils/proxyFetch.ts"; +import { getDefaultDispatcher, getRetryDispatcher } from "../../open-sse/utils/proxyDispatcher.ts"; + +const DIRECT_RESPONSE_START_TIMEOUT_CODE = "DIRECT_RESPONSE_START_TIMEOUT"; + +/** Simulates a silent half-open pooled socket: the request never resolves, but + * observes the abort signal like real undici does (rejects with the reason). */ +function hangingFetch(capture: { + calls: number; + dispatchers: unknown[]; +}): (input: RequestInfo | URL, init?: RequestInit) => Promise { + return (input, init) => { + capture.calls++; + capture.dispatchers.push((init as { dispatcher?: unknown } | undefined)?.dispatcher); + return new Promise((_, reject) => { + const signal = init?.signal; + signal?.addEventListener( + "abort", + () => + reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason))), + { once: true } + ); + // never resolve — the upstream accepted the connection but sends nothing + }); + }; +} + +function withFastTimeout(fn: () => Promise): Promise { + process.env.OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS = "50"; + return fn().finally(() => { + delete process.env.OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS; + }); +} + +test("#10214 a response-start timeout on the pooled attempt retries on the FRESH no-keep-alive dispatcher", async () => { + const capture = { calls: 0, dispatchers: [] as unknown[] }; + + const mockUndici = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + capture.calls++; + capture.dispatchers.push((init as { dispatcher?: unknown } | undefined)?.dispatcher); + if (capture.calls === 1) { + // First attempt hits a silently-dead pooled socket — hang, no error. + return new Promise((_, reject) => { + init?.signal?.addEventListener("abort", () => reject(init.signal!.reason), { once: true }); + }); + } + return new Response("ok", { status: 200 }); + }; + const mockNative = async (): Promise => + new Response("native-should-not-fire", { status: 200 }); + + const res = await withFastTimeout(() => + proxyFetch( + "https://opencode.ai/zen/go/v1/chat/completions", + { method: "POST" }, + { undiciFetch: mockUndici, nativeFetch: mockNative } + ) + ); + + assert.equal(capture.calls, 2, "pooled attempt times out and must retry once"); + assert.equal(await res.text(), "ok"); + // The regression guard: attempt 0 used the pooled keep-alive dispatcher; the + // retry used the fresh no-keep-alive dispatcher — a DIFFERENT instance, so the + // retry opens a brand-new socket that cannot be the zombie. + assert.equal( + capture.dispatchers[0], + getDefaultDispatcher(), + "first attempt must use the pooled default dispatcher" + ); + assert.equal( + capture.dispatchers[1], + getRetryDispatcher(), + "timeout retry must use the fresh no-keep-alive dispatcher" + ); + assert.notEqual(capture.dispatchers[0], capture.dispatchers[1]); +}); + +test("#10214 when the fresh-dispatcher retry also stalls, the timeout surfaces (no native fallback)", async () => { + const capture = { calls: 0, dispatchers: [] as unknown[] }; + const mockUndici = hangingFetch(capture); + const mockNative = async (): Promise => + new Response("native-should-not-fire", { status: 200 }); + + await assert.rejects( + withFastTimeout(() => + proxyFetch( + "https://opencode.ai/zen/go/v1/chat/completions", + { method: "POST" }, + { undiciFetch: mockUndici, nativeFetch: mockNative } + ) + ), + (err: unknown) => { + assert.equal( + (err as { code?: unknown }).code, + DIRECT_RESPONSE_START_TIMEOUT_CODE, + "final failure must be the classified direct response-start timeout" + ); + return true; + } + ); + assert.equal(capture.calls, 2, "both attempts must have been made"); + assert.equal(capture.dispatchers[0], getDefaultDispatcher()); + assert.equal(capture.dispatchers[1], getRetryDispatcher()); +}); + +test("#10214 a healthy fast response is untouched by the bound (single attempt, no retry)", async () => { + const capture = { calls: 0, dispatchers: [] as unknown[] }; + const mockUndici = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + capture.calls++; + capture.dispatchers.push((init as { dispatcher?: unknown } | undefined)?.dispatcher); + return new Response("ok", { status: 200 }); + }; + + const res = await withFastTimeout(() => + proxyFetch( + "https://opencode.ai/zen/go/v1/chat/completions", + { method: "POST" }, + { undiciFetch: mockUndici } + ) + ); + + assert.equal(capture.calls, 1, "healthy request must not retry"); + assert.equal(capture.dispatchers[0], getDefaultDispatcher()); + assert.equal(await res.text(), "ok"); +}); From 3c9cb21cca443b8caef5aa180827a6989e258a95 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Wed, 19 Aug 2026 15:14:55 -0300 Subject: [PATCH 002/135] feat(ad-hoc): add mesh interaction scripts for Discord integration --- scripts/ad-hoc/discord-en.json | 212 +++++++++++++++++++++++++++++ scripts/ad-hoc/mesh-run.mjs | 93 +++++++++++++ scripts/ad-hoc/mesh-send.mjs | 42 ++++++ scripts/ad-hoc/verify-coverage.mjs | 36 +++++ 4 files changed, 383 insertions(+) create mode 100644 scripts/ad-hoc/discord-en.json create mode 100644 scripts/ad-hoc/mesh-run.mjs create mode 100644 scripts/ad-hoc/mesh-send.mjs create mode 100644 scripts/ad-hoc/verify-coverage.mjs diff --git a/scripts/ad-hoc/discord-en.json b/scripts/ad-hoc/discord-en.json new file mode 100644 index 0000000000..c0d668592b --- /dev/null +++ b/scripts/ad-hoc/discord-en.json @@ -0,0 +1,212 @@ +[ + { + "bucket": "A", + "match": "failed to load external module playwright", + "text": "That error means the Playwright install shipped with OmniRoute is broken, not that you misconfigured anything. Reinstall with npm i -g omniroute and run npx playwright install chromium on the same host. See open-sse/executors/gemini-web.ts." + }, + { + "bucket": "A", + "match": "duckduckgo ai chat error", + "text": "That ERR_BAD_REQUEST usually means the model you picked is retired or unknown in Duck.ai's lineup, or a reasoningEffort setting the lineup doesn't accept. Try a current model like gpt-5.4-mini. See open-sse/executors/duckduckgo-web.ts." + }, + { + "bucket": "A", + "match": "what does endpoints do", + "text": "Endpoints are the OpenAI-compatible surface OmniRoute exposes. You point any client at base http://localhost:20128/v1 with your API key and it behaves like a normal provider. For opencode there is a dedicated guide at docs/frameworks/OPENCODE.md." + }, + { + "bucket": "A", + "match": "setup omniroute in opencode", + "text": "You don't need /connect. Run 'omniroute config opencode --base-url http://localhost:20128 --api-key YOUR_KEY' and point opencode at it. The most common bug is ending with /v1/v1, so keep a single /v1. See docs/frameworks/OPENCODE.md." + }, + { + "bucket": "A", + "match": "best way to integrate jules", + "text": "Use the Cloud Agents API: POST /api/v1/agents/tasks with providerId jules and OmniRoute spins a remote agent for that task. Selection is manual per task and control is via REST or the dashboard. See docs/frameworks/CLOUD_AGENT.md." + }, + { + "bucket": "A", + "match": "codex cloud and devin", + "text": "Same API, just swap the providerId: jules, devin, codex-cloud or cursor-cloud. Antigravity and Qwen are chat providers, not cloud agents, so they stay on chat routes. The choice is manual per task. See docs/frameworks/CLOUD_AGENT.md." + }, + { + "bucket": "A", + "match": "handle everything from claude", + "text": "Not quite. Cloud agents are controlled through the REST API and the dashboard, not through Claude Code or MCP. So keep them as separate tooling that talks to OmniRoute. See docs/frameworks/CLOUD_AGENT.md." + }, + { + "bucket": "A", + "match": "huggingchat returned http 500", + "text": "A 500 is a passthrough from the upstream HuggingChat endpoint (huggingface.co/chat), not something in your config. Just retry; if it keeps failing the service itself is likely having trouble. See open-sse/executors/huggingchat.ts." + }, + { + "bucket": "A", + "match": "use this on termux", + "text": "In Termux run 'pkg install nodejs' and then 'npx -y omniroute' to start the server. Your phone browser opens the dashboard over localhost afterwards. Walkthrough at docs/guides/TERMUX_GUIDE.md." + }, + { + "bucket": "A", + "match": "run the entire thing im on android", + "text": "You run everything in Termux with no root: pkg install nodejs, then npx -y omniroute starts the server. The dashboard opens in your phone's browser and all of it stays on the device." + }, + { + "bucket": "A", + "match": "i dont have omniroute", + "text": "Quick start: npm i -g omniroute on any machine with Node. Start it, open http://localhost:20128, and the auto model already answers so you don't even need an API key to try it." + }, + { + "bucket": "A", + "match": "api endpoints allowed", + "text": "Endpoints are their own API surface: anyone with a valid API key can call them. To lock it down, set REQUIRE_API_KEY=true so only the keys you issue get access. See docs/getting-started/QUICK-START.md." + }, + { + "bucket": "A", + "match": "need which host", + "text": "The host is wherever you run the server, localhost:20128 by default. Clients just need the base URL (http://host:20128/v1) plus an API key, so a VPS or Fly instance works the same." + }, + { + "bucket": "A", + "match": "hosting web in cpanel", + "text": "Self-host anywhere Node runs: a VPS, Docker or Fly.io. cPanel usually can't keep a long-running Node process alive, so prefer a real server or container. See docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md." + }, + { + "bucket": "A", + "match": "run fly.io docker file", + "text": "Use the repo's fly.toml: fly launch and then fly deploy, and the Dockerfile builds the image. Full steps and env vars are in docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md." + }, + { + "bucket": "A", + "match": "website https://fly.io", + "text": "Yes, the site runs on Fly.io, that's the host. From the repo run fly launch and fly deploy, and the app gets a public URL on a domain you own." + }, + { + "bucket": "A", + "match": "github are down", + "text": "You don't need GitHub to run OmniRoute. It installs straight from npm and you host it anywhere you want, a VPS, Docker, or Fly. GitHub matters only if you build from source." + }, + { + "bucket": "A", + "match": "2000 models is there a better way", + "text": "With that many models, Auto-Combo is the way: set the model to auto, auto/coding, auto/fast or auto/cheap and OmniRoute scores every option per request. The 14-factor scorer is in docs/routing/AUTO-COMBO.md." + }, + { + "bucket": "A", + "match": "is there a combo already", + "text": "Yes, there is a ready one for exactly this: auto/coding. It picks a good free coding model with no setup. The other auto strategies are explained in docs/routing/AUTO-COMBO.md." + }, + { + "bucket": "A", + "match": "where do i put auto", + "text": "You set it as the model field on your client exactly like a model name: auto/coding, or auto/fast and auto/cheap for other strategies. Their differences are in docs/routing/AUTO-COMBO.md." + }, + { + "bucket": "A", + "match": "dont see my combos as models", + "text": "Only auto/ combos are advertised in /v1/models. Custom combos are internal destinations that never appear in the list, so call them directly by the combo id you set up." + }, + { + "bucket": "A", + "match": "where is my circuit breaker", + "text": "It lives in the dashboard Health tab, in the circuit breaker states section, one status per provider. The closed, open, half-open model is in docs/architecture/RESILIENCE_GUIDE.md." + }, + { + "bucket": "A", + "match": "with claude desktop app", + "text": "Two ways: Claude Code pointed at OmniRoute via ANTHROPIC_BASE_URL plus setup-claude, or the Claude Desktop app as an MCP client via omniroute --mcp. Both are in docs/guides/CLAUDE-CODE-CONFIGURATION.md." + }, + { + "bucket": "A", + "match": "retrying in 30s", + "text": "That is a 429 rate limit from the provider, so retrying is expected. OmniRoute applies the cooldown and can fall back to another key or model automatically, so you don't need to touch anything." + }, + { + "bucket": "A", + "match": "cliproxyapi is configured", + "text": "It is informative, not an error. CLIProxyAPI is an upstream proxy layer, managed at runtime in the CLI Tools and toggled per provider between native, cliproxyapi and fallback modes. See docs/ops/PROXY_GUIDE.md." + }, + { + "bucket": "A", + "match": "getaddrinfo enotfound", + "text": "That is a doubled URL in the proxy registry: the host field carries the scheme. Use type=http, host=127.0.0.1 with no scheme, and port=20130. Steps are in docs/ops/PROXY_GUIDE.md." + }, + { + "bucket": "A", + "match": "proxy connection failed", + "text": "The registry expects type, host and port as separate fields, not one combined URL. Set host to 127.0.0.1 with no scheme and port to 20130, and the connection error clears. Same recipe in docs/ops/PROXY_GUIDE.md." + }, + { + "bucket": "B", + "match": "only 14 providers out of the 50", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "music play when i enable modal", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "do you mean the global proxy", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "provider's limits from docs", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "combine deepseek", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "store limits within the app", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "manually write these limits", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "official omniroute doesn't support", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "set limits in omniroute for a provider", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "create a compact prompt", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "continue your answer from where you left off", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "i am android that sorry", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "yes", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "C", + "match": "no such tool available: bash", + "text": "Here I only help with OmniRoute questions :)" + }, + { + "bucket": "C", + "match": "interupt the code", + "text": "Here I only help with OmniRoute questions :)" + } +] diff --git a/scripts/ad-hoc/mesh-run.mjs b/scripts/ad-hoc/mesh-run.mjs new file mode 100644 index 0000000000..95c45efd7d --- /dev/null +++ b/scripts/ad-hoc/mesh-run.mjs @@ -0,0 +1,93 @@ +// Runner generico para a mesh. Recebe um arquivo JSON de plano: +// [ +// { "bucket": "A"|"B"|"C", "match": "", "text": "" } +// ] +// Fases: A=reply (answered), B=notice mode:note (fica pending), C=recusa note + mark ignored (por ultimo). +// Envs: BOT_URL, BOT_TOKEN. Uso: node mesh-run.mjs +import { readFileSync } from "node:fs"; +import { env } from "node:process"; + +const BOT_URL = env.BOT_URL; +const BOT_TOKEN = env.BOT_TOKEN; +const FILTER = "platform=discord&language=en&direct=only"; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function api(path, opts = {}) { + const res = await fetch(BOT_URL + path, { + ...opts, + headers: { + Authorization: "Bearer " + BOT_TOKEN, + "Content-Type": "application/json", + ...(opts.headers || {}), + }, + }); + return { status: res.status, json: await res.json().catch(() => null) }; +} + +const planPath = process.argv[2]; +const plan = JSON.parse(readFileSync(planPath, "utf8")); + +async function main() { + console.log("Fetching pendentes (" + FILTER + ")..."); + const { json } = await api("/internal/bridge/questions?" + FILTER); + const pending = (json && json.data) || []; + console.log("-> " + pending.length + " pendentes"); + + const out = { A: [], B: [], C: [], U: [] }; + + // fase 1-2: A (reply) e B (notice) + for (const msg of pending) { + const t = (msg.text || "").toLowerCase(); + const hit = plan.find((e) => t.includes(e.match.toLowerCase())); + if (!hit) { + out.U.push(msg.id + " :: " + (msg.text || "").slice(0, 60)); + continue; + } + if (hit.bucket === "A") { + const r = await api("/internal/bridge/reply", { + method: "POST", + body: JSON.stringify({ messageId: msg.id, text: hit.text }), + }); + out.A.push(r.status + " " + msg.id); + } else if (hit.bucket === "B") { + const r = await api("/internal/bridge/reply", { + method: "POST", + body: JSON.stringify({ messageId: msg.id, text: hit.text, mode: "note" }), + }); + out.B.push(r.status + " " + msg.id); + } else if (hit.bucket === "C") { + out.C.push(msg.id); + } + await sleep(1000); + } + + // fase 3: bucket C — nota de recusa + mark ignored (por último) + for (const id of out.C) { + const msg = pending.find((m) => m.id === id); + const t = (msg.text || "").toLowerCase(); + const hit = plan.find((e) => e.bucket === "C" && t.includes(e.match.toLowerCase())); + if (!hit) continue; + const note = await api("/internal/bridge/reply", { + method: "POST", + body: JSON.stringify({ messageId: id, text: hit.text, mode: "note" }), + }); + const mark = await api("/internal/bridge/mark", { + method: "POST", + body: JSON.stringify({ messageIds: [id], status: "ignored", ref: "auto-declined" }), + }); + out.C[out.C.indexOf(id)] = + "note:" + note.status + " mark:" + (mark.json && mark.json.updated) + " " + id; + await sleep(1000); + } + + console.log("\n=== RESUMO ==="); + console.log("A (respondidas):", out.A); + console.log("B (notices, pending):", out.B); + console.log("C (recusa+ignoradas):", out.C); + console.log("U (nao classif., relatar):", out.U); +} + +main().catch((e) => { + console.error("ERRO:", e); + process.exit(1); +}); diff --git a/scripts/ad-hoc/mesh-send.mjs b/scripts/ad-hoc/mesh-send.mjs new file mode 100644 index 0000000000..8c6feb6de6 --- /dev/null +++ b/scripts/ad-hoc/mesh-send.mjs @@ -0,0 +1,42 @@ +// Helper único para enviar replies/notes no bridge do bot da mesh. +// Lê BOT_URL e BOT_TOKEN do ambiente (nunca embutidos). +// Uso: BOT_URL=... BOT_TOKEN=... node scripts/ad-hoc/mesh-send.mjs +// cmd: reply | note +import { readFileSync } from "node:fs"; + +const [cmd, path] = process.argv.slice(2); +const BOT_URL = process.env.BOT_URL; +const BOT_TOKEN = process.env.BOT_TOKEN; + +const input = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8"); +const items = JSON.parse(input); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function send(item) { + // endpoint de reply; mode presente => note + const body = { + messageId: item.id, + text: item.text, + ...(cmd === "note" ? { mode: "note" } : {}), + }; + const res = await fetch(`${BOT_URL}/internal/bridge/reply`, { + method: "POST", + headers: { + Authorization: `Bearer ${BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + const txt = await res.text(); + console.log(`[${cmd}] ${item.id.slice(0, 12)} → ${res.status} ${txt.slice(0, 80)}`); +} + +for (const item of items) { + try { + await send(item); + } catch (e) { + console.log(`[${cmd}] ${item.id.slice(0, 12)} → ERRO ${e.message}`); + } + await sleep(1000); // pace ~1s +} diff --git a/scripts/ad-hoc/verify-coverage.mjs b/scripts/ad-hoc/verify-coverage.mjs new file mode 100644 index 0000000000..05b05384d9 --- /dev/null +++ b/scripts/ad-hoc/verify-coverage.mjs @@ -0,0 +1,36 @@ +// Verificacao de cobertura do plano da mesh. +// Envs: BOT_URL, BOT_TOKEN. Uso: node verify-coverage.mjs +import { readFileSync } from "node:fs"; +import { env } from "node:process"; + +const BOT_URL = env.BOT_URL; +const BOT_TOKEN = env.BOT_TOKEN; +const FILTER = "platform=discord&language=en&direct=only"; + +const plan = JSON.parse(readFileSync(process.argv[2], "utf8")); + +const res = await fetch(BOT_URL + "/internal/bridge/questions?" + FILTER, { + headers: { Authorization: "Bearer " + BOT_TOKEN }, +}); +const json = await res.json(); +const pending = json.data || []; + +const gaps = []; +const amb = []; + +for (const m of pending) { + const t = (m.text || "").toLowerCase(); + const hits = plan.filter((e) => t.includes(e.match.toLowerCase())); + if (hits.length === 0) { + gaps.push(m.id + " :: " + t.slice(0, 80)); + } else if (hits.length > 1) { + const names = hits.map((h) => h.bucket + ":" + h.match).join(" | "); + amb.push(m.id + " :: " + names + " :: " + t.slice(0, 50)); + } +} + +console.log("pendentes:", pending.length, "| plano:", plan.length); +console.log("\n[GAPS] sem match (" + gaps.length + "):"); +for (const g of gaps) console.log(" -", g); +console.log("\n[AMB] >1 match (" + amb.length + "):"); +for (const a of amb) console.log(" -", a); From 3d7ed7aa87a316e392bb0d9bfb62ba1fe51dc43d Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 19 Aug 2026 17:58:58 -0300 Subject: [PATCH 003/135] fix(build): tolerate same-realpath symlink / stale-typed dest in assembleStandalone (#10776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under heavy concurrent build I/O, the bulk .build/next/standalone -> outDir tree copy can already have carried a prior pass's result into a NATIVE_ASSET_ENTRIES/EXTRA_MODULE_ENTRIES dest before that entry's own copy runs (an absolute pnpm-store symlink resolving to the exact same realpath as src, or a stale node of a different type). fs.cpSync/fs.cp refuse to overwrite either case even with force:true, throwing ERR_FS_CP_EINVAL ("src and dest cannot be the same") or ERR_FS_CP_DIR_TO_NON_DIR/ERR_FS_CP_NON_DIR_TO_DIR — non-deterministically crashing the build:release/build:cli deploy pipeline on whichever entry the race happened to hit that run. Adds resolvesToSamePath/clearStaleDest guards to all four copy call sites (the two sync loops in copyNativeAssetsAndExtraModules, repairEmptyExternalPackageDirs, and the async syncNativeAssetsToDir/syncExtraModulesToDir twins) so a dest already pointing at src is skipped and any other stale occupant is cleared before the fresh copy. Co-authored-by: Markus Hartung --- .../fixes/assemble-standalone-cpsync-race.md | 1 + scripts/build/assembleStandalone.mjs | 60 ++++++++++++++- tests/unit/build/assemble-standalone.test.ts | 73 +++++++++++++++++++ 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/assemble-standalone-cpsync-race.md diff --git a/changelog.d/fixes/assemble-standalone-cpsync-race.md b/changelog.d/fixes/assemble-standalone-cpsync-race.md new file mode 100644 index 0000000000..82fabbcad6 --- /dev/null +++ b/changelog.d/fixes/assemble-standalone-cpsync-race.md @@ -0,0 +1 @@ +- fix(build): tolerate a same-realpath symlink or stale-typed dest in the standalone bundle assembler, fixing non-deterministic `ERR_FS_CP_EINVAL`/`ERR_FS_CP_DIR_TO_NON_DIR` crashes under heavy concurrent build I/O diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index 069bd13f6d..ccdd61a654 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -347,7 +347,10 @@ async function syncNativeAssetsToDir(projectRoot, outDir, fsImpl, log) { if (!(await exists(sourcePath))) continue; const destinationPath = path.join(outDir, ...entry.dest); - if (path.resolve(sourcePath) === path.resolve(destinationPath)) continue; + // See resolvesToSamePath/clearStaleDest (sync copy path, same module) — the same + // ERR_FS_CP_EINVAL/ERR_FS_CP_DIR_TO_NON_DIR races apply to fsImpl.cp here. + if (resolvesToSamePath(sourcePath, destinationPath)) continue; + clearStaleDest(destinationPath); const mkdir = typeof fsImpl.mkdir === "function" ? fsImpl.mkdir.bind(fsImpl) : fs.mkdir.bind(fs); @@ -385,7 +388,8 @@ async function syncExtraModulesToDir(projectRoot, outDir, fsImpl, log) { if (!(await exists(sourcePath))) continue; const destPath = path.join(outDir, ...entry.dest); - if (path.resolve(sourcePath) === path.resolve(destPath)) continue; + if (resolvesToSamePath(sourcePath, destPath)) continue; + clearStaleDest(destPath); const mkdir = typeof fsImpl.mkdir === "function" ? fsImpl.mkdir.bind(fsImpl) : fs.mkdir.bind(fs); @@ -534,6 +538,46 @@ function copyStaticAndPublic({ distDir, relDistDir, projectRoot, resolvedOutDir } } +/** + * Two independent copy passes assemble a bundle: the bulk "standalone -> outDir" tree + * copy (step 1 of assembleStandalone) can already have carried a prior entry's result + * into `dest` (e.g. an absolute pnpm-store symlink, or a directory) BEFORE this entry's + * own copy runs. `fs.cpSync`/`fs.cp` refuse to overwrite in two such cases even with + * `force: true`: + * - dest already resolves (via symlink chain) to the exact same real path as src -> + * ERR_FS_CP_EINVAL "src and dest cannot be the same". + * - dest exists with a different node type than src (file/symlink vs directory) -> + * ERR_FS_CP_DIR_TO_NON_DIR / ERR_FS_CP_NON_DIR_TO_DIR. + * Under heavy concurrent build I/O this manifested non-deterministically across + * different EXTRA_MODULE_ENTRIES/NATIVE_ASSET_ENTRIES on every retry. Resolve both + * cases up front: skip entirely when dest is already the right target, otherwise clear + * whatever stale node occupies dest (via lstat, so it also removes a broken symlink) + * so the fresh copy always lands cleanly. + * + * @param {string} src + * @param {string} dest + * @returns {boolean} true when dest already IS src's target and no copy is needed + */ +function resolvesToSamePath(src, dest) { + if (path.resolve(src) === path.resolve(dest)) return true; + if (!fsSync.existsSync(dest)) return false; + try { + return fsSync.realpathSync(src) === fsSync.realpathSync(dest); + } catch { + return false; + } +} + +/** @see resolvesToSamePath — clears whatever stale node sits at `dest` before a copy. */ +function clearStaleDest(dest) { + try { + fsSync.lstatSync(dest); + } catch { + return; + } + fsSync.rmSync(dest, { recursive: true, force: true }); +} + /** * Copy native assets (better-sqlite3 and TPROXY) and extra runtime modules/sidecars * (wreq-js, pino, migrations, MITM server, helper scripts, sqlite-vec platform packages, …) @@ -547,7 +591,8 @@ function copyNativeAssetsAndExtraModules(projectRoot, resolvedOutDir) { const src = path.join(projectRoot, ...asset.src); if (!fsSync.existsSync(src)) continue; const dest = path.join(resolvedOutDir, ...asset.dest); - if (path.resolve(src) === path.resolve(dest)) continue; + if (resolvesToSamePath(src, dest)) continue; + clearStaleDest(dest); fsSync.mkdirSync(path.dirname(dest), { recursive: true }); fsSync.cpSync(src, dest, { recursive: true, force: true }); console.log(`[assembleStandalone] Copied native asset: ${asset.label}`); @@ -557,7 +602,8 @@ function copyNativeAssetsAndExtraModules(projectRoot, resolvedOutDir) { const src = path.join(projectRoot, ...mod.src); if (!fsSync.existsSync(src)) continue; const dest = path.join(resolvedOutDir, ...mod.dest); - if (path.resolve(src) === path.resolve(dest)) continue; + if (resolvesToSamePath(src, dest)) continue; + clearStaleDest(dest); fsSync.mkdirSync(path.dirname(dest), { recursive: true }); fsSync.cpSync(src, dest, { recursive: true, force: true }); console.log(`[assembleStandalone] Synced module: ${mod.label}`); @@ -617,6 +663,12 @@ function repairEmptyExternalPackageDirs(projectRoot, resolvedOutDir) { continue; } if (!sourceStat.isDirectory()) continue; + // See resolvesToSamePath/clearStaleDest above: bundlePkgDir can itself be a + // symlink to sourcePkgDir's realpath whose target momentarily read as empty + // under heavy concurrent build I/O (a transient readdirSync race, not a real + // hollow placeholder), or a stale non-directory node from an earlier pass. + if (resolvesToSamePath(sourcePkgDir, bundlePkgDir)) continue; + clearStaleDest(bundlePkgDir); fsSync.cpSync(sourcePkgDir, bundlePkgDir, { recursive: true, force: true }); summary.repaired += 1; diff --git a/tests/unit/build/assemble-standalone.test.ts b/tests/unit/build/assemble-standalone.test.ts index 14c7d890b9..14a8de854b 100644 --- a/tests/unit/build/assemble-standalone.test.ts +++ b/tests/unit/build/assemble-standalone.test.ts @@ -216,3 +216,76 @@ test("every relative import of standalone-server-ws.mjs is shipped into the bund } fs.rmSync(tmp, { recursive: true, force: true }); }); + +// Regression guard (deploy 2026-08-19): under heavy concurrent build I/O the bulk +// "standalone -> outDir" tree copy can already have carried a prior pass's result into +// an EXTRA_MODULE_ENTRIES/NATIVE_ASSET_ENTRIES `dest` BEFORE that entry's own copy runs +// — either an absolute symlink resolving to the exact same real path as `src` (a pnpm +// store layout), or a stale node of a different type (file/symlink vs directory). Node's +// fs.cpSync/fs.cp refuse both cases even with force:true, throwing ERR_FS_CP_EINVAL +// ("src and dest cannot be the same") or ERR_FS_CP_DIR_TO_NON_DIR/ERR_FS_CP_NON_DIR_TO_DIR +// respectively, crashing every one of copyNativeAssetsAndExtraModules, +// repairEmptyExternalPackageDirs, syncNativeAssetsToDir, and syncExtraModulesToDir. +test("copy passes tolerate a dest that already resolves to src, or a stale-typed dest", async () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-race-")); + const projectRoot = path.join(tmp, "src-root"); + seedSidecarSources(projectRoot); + + // Case 1 (sync path): dest already an absolute symlink resolving to src's realpath — + // simulates the wreq-js entry after the .build/next/standalone bulk copy already + // carried an absolute symlink over from an earlier standalone build. + const distDir = path.join(projectRoot, ".build/next"); + fs.mkdirSync(path.join(distDir, "standalone"), { recursive: true }); + fs.writeFileSync(path.join(distDir, "standalone", "server.js"), "// server"); + const outSync = path.join(tmp, "out-sync"); + fs.mkdirSync(path.join(outSync, "node_modules"), { recursive: true }); + fs.symlinkSync( + path.join(projectRoot, "node_modules/wreq-js"), + path.join(outSync, "node_modules/wreq-js") + ); + // Case 2 (sync path): dest already a plain FILE where src is a directory — + // simulates @swc/helpers landing as a stray file from an unrelated earlier copy. + fs.mkdirSync(path.join(outSync, "node_modules/@swc"), { recursive: true }); + fs.writeFileSync(path.join(outSync, "node_modules/@swc/helpers"), "stale file, not a dir"); + + assert.doesNotThrow(() => { + assembleStandalone({ + distDir, + outDir: outSync, + projectRoot, + sanitizePaths: false, + copyNatives: true, + }); + }, "assembleStandalone must not throw on a same-realpath symlink or a stale-typed dest"); + + assert.ok( + fs.existsSync(path.join(outSync, "node_modules/wreq-js/rust/lib.so")), + "wreq-js content reachable through the pre-existing symlink" + ); + assert.ok( + fs.statSync(path.join(outSync, "node_modules/@swc/helpers")).isDirectory(), + "the stale file at @swc/helpers was replaced by the real directory" + ); + assert.ok( + fs.existsSync(path.join(outSync, "node_modules/@swc/helpers/package.json")), + "@swc/helpers content copied after clearing the stale file" + ); + + // Case 3 (async path): same real-path-symlink collision hits syncStandaloneExtraModules. + const outAsync = path.join(tmp, "out-async"); + fs.mkdirSync(path.join(outAsync, "node_modules"), { recursive: true }); + fs.symlinkSync( + path.join(projectRoot, "node_modules/sql.js"), + path.join(outAsync, "node_modules/sql.js") + ); + await assert.doesNotReject( + () => syncStandaloneExtraModules(projectRoot, fs.promises, { log() {} }, outAsync), + "syncStandaloneExtraModules must not throw on a same-realpath symlink" + ); + assert.ok( + fs.existsSync(path.join(outAsync, "node_modules/sql.js/dist/sql-wasm.js")), + "sql.js content reachable through the pre-existing symlink" + ); + + fs.rmSync(tmp, { recursive: true, force: true }); +}); From 41ffb08e4a4a4f14e8178641c7874712789a038d Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 00:50:35 -0300 Subject: [PATCH 004/135] chore(quality): rebaseline file-size for merge-train batch1 (#10722, #10797) + pre-existing chatBodyAdmission.ts drift Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- config/quality/file-size-baseline.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 05323c8002..96001706ef 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -442,12 +442,14 @@ "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1062, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051, "src/shared/components/ModelSelectModal.tsx": 1138, - "src/shared/constants/providers/apikey/gateways.ts": 1255, + "src/shared/constants/providers/apikey/gateways.ts": 1268, "open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387, "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "src/lib/modelCapabilities.ts": 1006, "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014, - "open-sse/config/imageRegistry.ts": 1019 + "open-sse/config/imageRegistry.ts": 1019, + "src/sse/handlers/chatHelpers.ts": 1017, + "src/shared/middleware/chatBodyAdmission.ts": 1005 }, "_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.", "_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).", @@ -612,5 +614,6 @@ "_rebaseline_2026_08_12_proxyfetch_redaction": "Base-reds round 3 (#9985): proxyFetch.ts 1220->1239 (+19) = redactProxyDetailsInMessage() helper closing the credential leak #10032 reintroduced (raw proxy URL with user:password appended to the propagated error, Hard Rule #12); irreducible security fix at the existing error-surface chokepoint. Covered by tests/unit/tls-proxy-context.test.ts (strengthened leak guards).", "_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.", "_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).", - "_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests)." -} + "_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).", + "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session)." +} \ No newline at end of file From 81b0ff46a3b4167bf64582a87e4be70ee4543c39 Mon Sep 17 00:00:00 2001 From: Ara Date: Thu, 20 Aug 2026 02:28:29 -0700 Subject: [PATCH 005/135] fix(cline): label internal health checks (#10706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution! --- src/shared/utils/clineAuth.ts | 21 +++++++++++++++++-- .../cline-workos-auth-token-shape.test.ts | 13 ++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/shared/utils/clineAuth.ts b/src/shared/utils/clineAuth.ts index c8bab6214c..b923332fa0 100644 --- a/src/shared/utils/clineAuth.ts +++ b/src/shared/utils/clineAuth.ts @@ -13,6 +13,8 @@ import { randomUUID } from "node:crypto"; import { APP_CONFIG } from "../constants/appConfig"; const APP_VERSION = APP_CONFIG.version; +const DEFAULT_CLINE_CLIENT_TYPE = "omniroute"; +const INTERNAL_HEALTH_CHECK_CLIENT_TYPE = "omniroute-internal-health-check"; export interface ClineHeaderContext { taskId?: string; @@ -44,6 +46,12 @@ export function resolveClineTaskId(clientHeaders?: Record | null return getHeaderCaseInsensitive(clientHeaders, "x-task-id") ?? randomUUID(); } +function resolveClineClientType(clientHeaders?: Record | null): string | undefined { + return getHeaderCaseInsensitive(clientHeaders, "x-internal-test") === "combo-health-check" + ? INTERNAL_HEALTH_CHECK_CLIENT_TYPE + : undefined; +} + /** * Apply the required Cline billing headers with case-insensitive replacement. * These fields are authoritative in the official client and must win over @@ -58,12 +66,18 @@ export function applyClineProtocolHeaders( getHeaderCaseInsensitive(headers, "x-task-id") ?? randomUUID(); const clientVersion = cleanHeaderValue(context.clientVersion) ?? APP_VERSION; + const existingClientType = getHeaderCaseInsensitive(headers, "x-client-type"); + const clientType = + cleanHeaderValue(context.clientType) ?? + (existingClientType === INTERNAL_HEALTH_CHECK_CLIENT_TYPE + ? INTERNAL_HEALTH_CHECK_CLIENT_TYPE + : DEFAULT_CLINE_CLIENT_TYPE); const required: Record = { "HTTP-Referer": "https://cline.bot", "X-Title": "Cline", "User-Agent": `Cline/${clientVersion}`, "X-IS-MULTIROOT": context.isMultiRoot === true ? "true" : "false", - "X-CLIENT-TYPE": cleanHeaderValue(context.clientType) ?? "omniroute", + "X-CLIENT-TYPE": clientType, "X-CLIENT-VERSION": clientVersion, "X-PLATFORM": cleanHeaderValue(context.platform) ?? process.platform ?? "unknown", "X-PLATFORM-VERSION": cleanHeaderValue(context.platformVersion) ?? process.version ?? "unknown", @@ -159,7 +173,10 @@ export function applyClineAuthHeaders( clientHeaders: Record | null | undefined, isClinepass: boolean ): Record { - const context: ClineHeaderContext = { taskId: resolveClineTaskId(clientHeaders) }; + const context: ClineHeaderContext = { + taskId: resolveClineTaskId(clientHeaders), + clientType: resolveClineClientType(clientHeaders), + }; const built = isClinepass ? buildClinepassHeaders(credentials, effectiveKey, context) : buildClineHeaders(effectiveKey || credentials?.accessToken, {}, context); diff --git a/tests/unit/cline-workos-auth-token-shape.test.ts b/tests/unit/cline-workos-auth-token-shape.test.ts index 94c73148cd..ab637ac930 100644 --- a/tests/unit/cline-workos-auth-token-shape.test.ts +++ b/tests/unit/cline-workos-auth-token-shape.test.ts @@ -110,3 +110,16 @@ test("DefaultExecutor.buildHeaders uses the cline workos auth token shape", () = assert.equal(headers["X-Title"], "Cline"); assert.equal(headers["X-Task-ID"], "task-from-client"); }); + +test("DefaultExecutor labels internal health checks separately from user traffic", () => { + const executor = new DefaultExecutor("cline"); + const headers = executor.buildHeaders({ apiKey: "tok-abc" }, true, { + "X-Internal-Test": "combo-health-check", + }); + + assert.equal(headers["X-CLIENT-TYPE"], "omniroute-internal-health-check"); + + // BaseExecutor reapplies the required protocol headers immediately before dispatch. + applyClineProtocolHeaders(headers, { taskId: headers["X-Task-ID"] }); + assert.equal(headers["X-CLIENT-TYPE"], "omniroute-internal-health-check"); +}); From 1accabeb4ea1aaf52ec708fbc3228b375840069f Mon Sep 17 00:00:00 2001 From: Webman Date: Thu, 20 Aug 2026 04:28:33 -0500 Subject: [PATCH 006/135] fix(db): prevent Windows native-driver hang from stalling requests (#10627) (#10709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution! --- src/lib/db/adapters/driverFactory.ts | 79 ++++++++++++- src/proxy.ts | 19 +++ tests/unit/db-adapters/driverFactory.test.ts | 118 +++++++++++++++++++ 3 files changed, 213 insertions(+), 3 deletions(-) diff --git a/src/lib/db/adapters/driverFactory.ts b/src/lib/db/adapters/driverFactory.ts index d696d1388d..134e984c28 100644 --- a/src/lib/db/adapters/driverFactory.ts +++ b/src/lib/db/adapters/driverFactory.ts @@ -1,5 +1,6 @@ import { runtimeRequire as _require } from "./runtimeRequire"; import { existsSync } from "node:fs"; +import { spawnSync } from "node:child_process"; import { createBetterSqliteAdapter } from "./betterSqliteAdapter"; import { createBunSqliteAdapter, type BunSqliteDatabaseLike } from "./bunSqliteAdapter"; import { @@ -11,6 +12,70 @@ import type { SqliteAdapter } from "./types"; type DriverLoader = (moduleName: string) => unknown; +type SpawnSyncLike = ( + command: string, + args: string[], + options: { timeout: number; stdio: "ignore"; cwd: string; windowsHide: boolean } +) => { status: number | null }; + +/** Returns whether better-sqlite3 may be loaded in this process. */ +export type DriverProbe = () => boolean; + +/** + * #10627 — Windows driver-hang guard. + * + * The sync cascade's try/catch only covers drivers that THROW on load + * (ERR_DLOPEN_FAILED, "Module did not self-register", ...). On Windows, a + * mismatched-ABI native addon can HANG inside DllMain (loader lock) instead of + * throwing — a hang never reaches the catch, so the fallback to node:sqlite / + * sql.js never runs and the first DB touch in a runtime stalls forever at ~0% + * CPU (the exact #10627 symptom: every request hangs, 0 bytes, no logs). + * + * The probe answers "can better-sqlite3 load AND open a database?" by loading + * it in a CHILD PROCESS with a bounded timeout, so a hang becomes a timed-out + * probe (verdict "bad") instead of a process-level deadlock. The verdict is + * cached per process — the child spawn happens at most once. + * + * On POSIX this is a no-op returning true: broken addons throw there, which + * the existing cascade already handles, and we don't want to pay a subprocess + * spawn on every Linux/CI boot. + */ +export function createBetterSqliteProbe(options: { + platform?: string; + execPath?: string; + spawn?: SpawnSyncLike; + timeoutMs?: number; +}): DriverProbe { + const { + platform = process.platform, + execPath = process.execPath, + spawn = spawnSync as unknown as SpawnSyncLike, + timeoutMs = 5_000, + } = options; + + let verdict: boolean | null = null; + return () => { + if (verdict !== null) return verdict; + if (platform !== "win32") { + verdict = true; + return verdict; + } + try { + const result = spawn(execPath, ["-e", "require('better-sqlite3')(':memory:')"], { + timeout: timeoutMs, + stdio: "ignore", + cwd: process.cwd(), + windowsHide: true, + }); + // status === null means the child was killed by the timeout — a hang. + verdict = result.status === 0; + } catch { + verdict = false; + } + return verdict; + }; +} + /** * The production loader for the sync driver cascade. * @@ -137,7 +202,12 @@ function getSqlJsPendingCache(): Map> { * Builds the synchronous driver cascade. Keeping the loader injectable makes * the real node:sqlite branch testable without changing the public adapter API. */ -export function createSyncDriverFactory(load: DriverLoader) { +export function createSyncDriverFactory(load: DriverLoader, betterSqliteProbe?: DriverProbe) { + // #10627: when a probe is supplied, the better-sqlite3 branch is gated on it + // so a Windows DllMain hang (which never throws, so never hits the catch) + // cannot stall the request path. Default: no probe — existing callers/tests + // keep the historical throw-only behavior. + const mayLoadBetterSqlite = betterSqliteProbe ?? (() => true); return function tryOpenSync( filePath: string, options?: Record @@ -164,7 +234,7 @@ export function createSyncDriverFactory(load: DriverLoader) { } // better-sqlite3: rápido, nativo — skip em Bun - if (!process.versions.bun) { + if (!process.versions.bun && mayLoadBetterSqlite()) { try { const BetterSqlite = load("better-sqlite3") as { new (p: string, o?: object): import("better-sqlite3").Database; @@ -204,7 +274,10 @@ export function createSyncDriverFactory(load: DriverLoader) { }; } -const openSyncDriver = createSyncDriverFactory(requireSqliteDriver); +// Production wiring: the real probe (child-process, timed, cached) guards the +// better-sqlite3 branch so a hang on Windows degrades to a failover instead of +// a request-path deadlock (#10627). +const openSyncDriver = createSyncDriverFactory(requireSqliteDriver, createBetterSqliteProbe({})); /** * The installed-tarball smoke uses this paired marker to exercise the sql.js tier diff --git a/src/proxy.ts b/src/proxy.ts index 93de5e149f..153785efdf 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,6 +1,25 @@ import type { NextRequest } from "next/server"; import { runAuthzPipeline } from "./server/authz/pipeline"; +// #10627: the proxy runs in its own Next.js runtime and never executes +// instrumentation-node.ts's startup warm-ups, so its FIRST request used to +// trigger a cold `import("@/lib/db/settings")` → native SQLite driver load ON +// the request path. If that addon hangs (see driverFactory's #10627 probe), +// every proxied request stalled indefinitely with 0 bytes and no logs. +// Warm the settings cache here at boot instead: a driver failure now surfaces +// as a logged startup error, and real requests start with a hot cache. +// Fire-and-forget — never blocks proxy initialization, never rejects the +// module (mirrors the `void warmModelCatalogCache()` pattern in +// instrumentation-node.ts). +void import("./lib/db/readCache") + .then(({ getCachedSettings }) => getCachedSettings()) + .catch((err: unknown) => { + console.error( + "[proxy] DB settings warm failed; requests will use default limits:", + err instanceof Error ? err.message : err + ); + }); + export async function proxy(request: NextRequest) { return runAuthzPipeline(request, { enforce: true }); } diff --git a/tests/unit/db-adapters/driverFactory.test.ts b/tests/unit/db-adapters/driverFactory.test.ts index f5fc774e3f..b73ea87a6f 100644 --- a/tests/unit/db-adapters/driverFactory.test.ts +++ b/tests/unit/db-adapters/driverFactory.test.ts @@ -10,6 +10,7 @@ import { runtimeRequire } from "../../../src/lib/db/adapters/runtimeRequire.ts"; const { createSyncDriverFactory, + createBetterSqliteProbe, isPackBootForcedSqlJsSmoke, tryOpenSync, openDatabaseAsync, @@ -81,6 +82,55 @@ describe("driverFactory", () => { } ); + test("rejected probe skips better-sqlite3 and falls through to node:sqlite", (t) => { + const databasePath = createTempDatabasePath(t); + const openWithoutBrokenAddon = createSyncDriverFactory( + (moduleName: string) => { + if (moduleName === "better-sqlite3") { + throw new Error("better-sqlite3 must not load when the probe rejects it"); + } + return require(moduleName); + }, + () => false + ); + + const adapter = openWithoutBrokenAddon(databasePath); + assert.ok(adapter); + assert.equal(adapter.driver, "node:sqlite"); + adapter.exec("CREATE TABLE items (value TEXT)"); + adapter.prepare("INSERT INTO items VALUES (?)").run("ok"); + assert.equal( + (adapter.prepare("SELECT value FROM items").get() as { value: string }).value, + "ok" + ); + adapter.close(); + }); + + test("passed probe still prefers better-sqlite3 in the cascade", () => { + let betterSqliteRequested = false; + const openWithPassedProbe = createSyncDriverFactory( + (moduleName: string) => { + if (moduleName === "better-sqlite3") { + betterSqliteRequested = true; + return function FakeBetterSqlite() { + return { close() {}, name: ":memory:", open: true }; + }; + } + if (moduleName === "node:sqlite") { + throw new Error("node:sqlite must not load when better-sqlite3 passes the probe"); + } + throw new Error(`unexpected driver load: ${moduleName}`); + }, + () => true + ); + + const adapter = openWithPassedProbe(":memory:"); + assert.ok(adapter); + assert.equal(adapter.driver, "better-sqlite3"); + assert.equal(betterSqliteRequested, true); + adapter.close(); + }); + test("prefers better-sqlite3 before node:sqlite in the driver cascade", () => { const fakeBetterSqlite = { close() {}, @@ -337,6 +387,74 @@ describe("driverFactory", () => { }); } + // #10627 — the Windows driver-hang guard. On Windows a mismatched-ABI + // better-sqlite3 addon can HANG inside DllMain instead of throwing, so the + // cascade's try/catch never fires and the fallback never runs. The probe + // loads the addon in a child process with a bounded timeout, turning a hang + // into a cached "bad" verdict that skips the branch. + test("probe: non-Windows platforms skip the child probe and report ok", () => { + let spawned = 0; + const probe = createBetterSqliteProbe({ + platform: "linux", + execPath: "node", + spawn: () => { + spawned += 1; + return { status: 0 }; + }, + }); + assert.equal(probe(), true); + assert.equal(probe(), true); + assert.equal(spawned, 0, "POSIX must not spawn a probe child process"); + }); + + test("probe: successful child probe is cached (spawned at most once)", () => { + let spawned = 0; + const probe = createBetterSqliteProbe({ + platform: "win32", + execPath: "node", + spawn: () => { + spawned += 1; + return { status: 0 }; + }, + }); + assert.equal(probe(), true); + assert.equal(probe(), true); + assert.equal(probe(), true); + assert.equal(spawned, 1, "verdict must be cached per process"); + }); + + test("probe: non-zero child exit rejects better-sqlite3", () => { + const probe = createBetterSqliteProbe({ + platform: "win32", + execPath: "node", + spawn: () => ({ status: 1 }), + }); + assert.equal(probe(), false); + assert.equal(probe(), false); + }); + + test("probe: child spawn throw rejects better-sqlite3", () => { + const probe = createBetterSqliteProbe({ + platform: "win32", + execPath: "node", + spawn: () => { + throw new Error("spawn failed"); + }, + }); + assert.equal(probe(), false); + }); + + test("probe: timed-out child (status null) rejects better-sqlite3 — the #10627 hang case", () => { + // status === null is exactly what spawnSync returns when the child is + // killed by the timeout — i.e. the DllMain hang that never throws. + const probe = createBetterSqliteProbe({ + platform: "win32", + execPath: "node", + spawn: () => ({ status: null }), + }); + assert.equal(probe(), false); + }); + test("retains the existing cascade when native drivers are unavailable", () => { const openWithoutNativeDrivers = createSyncDriverFactory(() => { throw new Error("forced driver load failure"); From 82e5afed6b528acfda29841c01b8e05ee593591e Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:28:37 +0800 Subject: [PATCH 007/135] feat(usage): show Kimi Coding Extra Usage (#10712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution! --- .../features/kimi-coding-extra-usage.md | 1 + open-sse/services/usage/kimi.ts | 211 +++++++-- .../components/ProviderLimits/QuotaCard.tsx | 13 +- .../parts/QuotaCardExpanded.tsx | 34 +- .../components/ProviderLimits/quotaParsing.ts | 21 +- .../usage/components/ProviderLimits/utils.tsx | 4 +- src/i18n/messages/en.json | 10 + src/i18n/messages/pt-BR.json | 10 + src/i18n/messages/vi.json | 10 + src/i18n/messages/zh-CN.json | 10 + src/i18n/messages/zh-TW.json | 10 + src/lib/db/providerLimits.ts | 11 +- src/lib/usage/providerLimitsCache.ts | 18 +- src/shared/utils/kimiBilling.ts | 199 +++++++++ src/shared/utils/providerBilling.ts | 36 ++ tests/unit/kimi-coding-billing-ui.test.ts | 159 +++++++ tests/unit/kimi-coding-billing.test.ts | 419 ++++++++++++++++++ ...ota-card-expanded-fixed-order-6687.test.ts | 53 +++ .../repro-7764-collapsed-quota-order.test.ts | 18 + tests/unit/usage-service-hardening.test.ts | 5 +- 20 files changed, 1181 insertions(+), 71 deletions(-) create mode 100644 changelog.d/features/kimi-coding-extra-usage.md create mode 100644 src/shared/utils/kimiBilling.ts create mode 100644 src/shared/utils/providerBilling.ts create mode 100644 tests/unit/kimi-coding-billing-ui.test.ts create mode 100644 tests/unit/kimi-coding-billing.test.ts diff --git a/changelog.d/features/kimi-coding-extra-usage.md b/changelog.d/features/kimi-coding-extra-usage.md new file mode 100644 index 0000000000..766ec1020c --- /dev/null +++ b/changelog.d/features/kimi-coding-extra-usage.md @@ -0,0 +1 @@ +- **feat(usage):** show Kimi Coding's fixed-order Code 5-hour/7-day quota windows plus Extra Usage status, balance, monthly spend/limit, and the official Additional Credits link on Dashboard → Quota cards. diff --git a/open-sse/services/usage/kimi.ts b/open-sse/services/usage/kimi.ts index ca9f2d5630..d27c3c889b 100644 --- a/open-sse/services/usage/kimi.ts +++ b/open-sse/services/usage/kimi.ts @@ -9,12 +9,16 @@ */ import { safePercentage } from "@/shared/utils/formatting"; +import { + KIMI_CODE_ADDITIONAL_CREDITS_URL, + type KimiBillingStatus, +} from "@/shared/utils/kimiBilling"; import { buildKimiCodeIdentityHeaders, getKimiCodeCliUserAgent, } from "../../config/providers/registry/kimi/coding/runtime.ts"; import { toRecord, toNumber } from "./scalars.ts"; -import { type UsageQuota, parseResetTime } from "./quota.ts"; +import { createQuotaFromUsage, type UsageQuota, parseResetTime } from "./quota.ts"; type JsonRecord = Record; @@ -25,6 +29,145 @@ const KIMI_CONFIG = { apiVersion: "2023-06-01", }; +const KIMI_BOOSTER_FIXED_POINT_PER_CENT = 1_000_000; + +function toInteger(value: unknown): number | null { + const parsed = toNumber(value, Number.NaN); + return Number.isFinite(parsed) ? Math.trunc(parsed) : null; +} + +function fixedPointToCents(value: number): number { + const cents = value / KIMI_BOOSTER_FIXED_POINT_PER_CENT; + if (cents > 0 && cents < 1) return 1; + return Math.round(cents); +} + +function parseKimiMoney(value: unknown): { cents: number; currency: string } | null { + const money = toRecord(value); + const cents = toInteger(money.priceInCents); + const currency = money.currency; + if ( + cents === null || + cents < 0 || + typeof currency !== "string" || + !/^[A-Za-z]{3}$/.test(currency) + ) { + return null; + } + return { cents, currency: currency.toUpperCase() }; +} + +function parseKimiExtraUsageStatus(value: unknown): KimiBillingStatus["extraUsageStatus"] { + switch (value) { + case "STATUS_ACTIVE": + return "enabled"; + case "STATUS_DISABLED": + return "disabled"; + case "STATUS_FROZEN": + return "frozen"; + default: + return "unavailable"; + } +} + +function parseKimiBoosterWallet(value: unknown): KimiBillingStatus | null { + const wallet = toRecord(value); + const balance = toRecord(wallet.balance); + if (balance.type !== "BOOSTER") return null; + + const amount = toInteger(balance.amount); + const amountLeft = toInteger(balance.amountLeft); + const monthlyLimit = parseKimiMoney(wallet.monthlyChargeLimit); + const monthlyUsed = parseKimiMoney(wallet.monthlyUsed); + const autoRefillCharge = parseKimiMoney(wallet.autoRefillCharge); + const autoRefillThreshold = parseKimiMoney(wallet.autoRefillThreshold); + const extraUsageStatus = parseKimiExtraUsageStatus(wallet.status); + const hasWalletEvidence = + (amount !== null && amount > 0) || + amountLeft !== null || + monthlyLimit !== null || + monthlyUsed !== null || + extraUsageStatus !== "unavailable"; + if (!hasWalletEvidence) return null; + + const currency = + monthlyLimit?.currency ?? + monthlyUsed?.currency ?? + autoRefillCharge?.currency ?? + autoRefillThreshold?.currency ?? + "USD"; + + return { + currency, + // Proto JSON omits numeric zero values. Production therefore returns a + // BOOSTER balance record without amount/amountLeft when the preserved + // balance is exactly zero; treat that as an explicit zero, not unknown. + extraCreditsMinorUnits: + amountLeft === null || amountLeft < 0 ? 0 : fixedPointToCents(amountLeft), + monthlyUsedMinorUnits: monthlyUsed?.cents ?? 0, + monthlyLimitEnabled: wallet.monthlyChargeLimitEnabled === true, + monthlyLimitMinorUnits: monthlyLimit?.cents ?? 0, + extraUsageStatus, + additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL, + }; +} + +function buildKimiBillingStatus(value: unknown): KimiBillingStatus { + return ( + parseKimiBoosterWallet(value) ?? { + currency: "USD", + extraUsageStatus: "unavailable", + additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL, + } + ); +} + +function optionalNumber(value: unknown): number | null { + if (typeof value !== "number" && typeof value !== "string") return null; + const parsed = toNumber(value, Number.NaN); + return Number.isFinite(parsed) ? parsed : null; +} + +function createKimiCountQuota(value: unknown): UsageQuota | null { + const detail = toRecord(value); + const limit = optionalNumber(detail.limit ?? detail.Limit); + if (limit === null || limit <= 0) return null; + + const reportedUsed = optionalNumber(detail.used ?? detail.Used); + const reportedRemaining = optionalNumber(detail.remaining ?? detail.Remaining); + const used = reportedUsed ?? (reportedRemaining === null ? 0 : limit - reportedRemaining); + return createQuotaFromUsage(used, limit, detail.resetTime ?? detail.reset_at ?? detail.resetAt); +} + +type KimiWindowLabel = { key: string; displayName: string }; + +function normalizeKimiWindow(value: unknown, fallbackIndex: number): KimiWindowLabel { + const window = toRecord(value); + const duration = optionalNumber(window.duration); + const timeUnit = window.timeUnit; + + if (duration !== null && duration > 0) { + if (timeUnit === "TIME_UNIT_MINUTE" && duration % 60 === 0) { + const hours = duration / 60; + return { key: `${hours}h`, displayName: `Code · ${hours}h` }; + } + if (timeUnit === "TIME_UNIT_HOUR") { + return { key: `${duration}h`, displayName: `Code · ${duration}h` }; + } + if (timeUnit === "TIME_UNIT_DAY") { + return { key: `${duration}d`, displayName: `Code · ${duration}d` }; + } + if (timeUnit === "TIME_UNIT_WEEK") { + return { key: `${duration}w`, displayName: `Code · ${duration}w` }; + } + if (timeUnit === "TIME_UNIT_MINUTE") { + return { key: `${duration}m`, displayName: `Code · ${duration}m` }; + } + } + + return { key: `limit_${fallbackIndex}`, displayName: `Code · Limit ${fallbackIndex}` }; +} + /** * Map Kimi membership level to display name * LEVEL_BASIC = Moderato, LEVEL_INTERMEDIATE = Allegretto, @@ -100,52 +243,38 @@ export async function getKimiUsage( const quotas: Record = {}; const dataObj = toRecord(data); + const billing = buildKimiBillingStatus(dataObj.boosterWallet); - // Parse Kimi usage response format - // Format: { user: {...}, usage: { limit: "100", used: "92", remaining: "8", resetTime: "..." }, limits: [...] } - const usageObj = toRecord(dataObj.usage); - - // Check for Kimi's actual usage fields (strings, not numbers) - const usageLimit = toNumber(usageObj.limit || usageObj.Limit, 0); - const usageUsed = toNumber(usageObj.used || usageObj.Used, 0); - const usageRemaining = toNumber(usageObj.remaining || usageObj.Remaining, 0); - const usageResetTime = - usageObj.resetTime || usageObj.ResetTime || usageObj.reset_at || usageObj.resetAt; - - if (usageLimit > 0) { - const percentRemaining = usageLimit > 0 ? (usageRemaining / usageLimit) * 100 : 0; - - quotas["Weekly"] = { - used: usageUsed, - total: usageLimit, - remaining: usageRemaining, - remainingPercentage: percentRemaining, - resetAt: parseResetTime(usageResetTime), - unlimited: false, - }; + // The managed Kimi Code API reports the Code 7-day quota in `usage`. + // The website's separate shared-membership total/Kimi split comes from a + // Web-session-only endpoint and cannot be read with a Coding OAuth token. + const weeklyQuota = createKimiCountQuota(dataObj.usage); + if (weeklyQuota) { + quotas.code_7d = { ...weeklyQuota, displayName: "Code · 7d" }; } - // Also parse limits array for rate limits + // Each limits[] item is an independent rolling window. Preserve all of + // them with deterministic window-derived keys instead of overwriting one + // generic `Ratelimit` row. const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : []; for (let i = 0; i < limitsArray.length; i++) { const limitItem = toRecord(limitsArray[i]); - const window = toRecord(limitItem.window); - const detail = toRecord(limitItem.detail); + const quota = createKimiCountQuota(limitItem.detail); + if (!quota) continue; - const limit = toNumber(detail.limit || detail.Limit, 0); - const remaining = toNumber(detail.remaining || detail.Remaining, 0); - const resetTime = detail.resetTime || detail.reset_at || detail.resetAt; - - if (limit > 0) { - quotas["Ratelimit"] = { - used: limit - remaining, - total: limit, - remaining, - remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0, - resetAt: parseResetTime(resetTime), - unlimited: false, - }; - } + const normalized = normalizeKimiWindow(limitItem.window, i + 1); + const baseKey = `code_${normalized.key}`; + let key = baseKey; + let suffix = 2; + while (key in quotas) key = `${baseKey}_${suffix++}`; + const reportedName = + typeof limitItem.name === "string" && limitItem.name.trim() ? limitItem.name.trim() : null; + const displayName = reportedName + ? /^code\b/i.test(reportedName) + ? reportedName + : `Code · ${reportedName}` + : normalized.displayName; + quotas[key] = { ...quota, displayName }; } // Check for quota windows (Claude-like format with utilization) as fallback @@ -189,6 +318,7 @@ export async function getKimiUsage( return { plan: planName || "Kimi Coding", quotas, + billing, }; } @@ -199,6 +329,7 @@ export async function getKimiUsage( return { plan: planName || "Kimi Coding", message: "Kimi Coding connected. Usage tracked per request.", + billing, }; } catch (error) { return { diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx index 7859c0b008..af76b84a77 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCard.tsx @@ -2,7 +2,10 @@ import { useMemo, useState } from "react"; import Card from "@/shared/components/Card"; -import type { GrokBillingStatus } from "@/shared/utils/grokBilling"; +import { + isProviderBillingProvider, + type ProviderBillingStatus, +} from "@/shared/utils/providerBilling"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; import { normalizePlanTier, @@ -35,8 +38,8 @@ interface QuotaCardProps { quotas?: any[]; plan?: string | null; message?: string | null; - billing?: GrokBillingStatus | null; - raw?: { billing?: GrokBillingStatus | null }; + billing?: ProviderBillingStatus | null; + raw?: { billing?: ProviderBillingStatus | null }; stale?: { since?: string; reason?: string } | null; } | undefined; @@ -151,7 +154,9 @@ export default function QuotaCard({ error={error} message={quota?.message ?? null} billing={ - connection.provider === "grok-cli" ? (quota?.billing ?? quota?.raw?.billing) : null + isProviderBillingProvider(connection.provider) + ? (quota?.billing ?? quota?.raw?.billing) + : null } refreshedAt={displayRefreshedAt} hasStaleData={hasStaleData} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx index 25e47a8741..60346dc03b 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded.tsx @@ -2,7 +2,13 @@ import { useMemo, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; -import { buildGrokBillingCardRows, type GrokBillingStatus } from "@/shared/utils/grokBilling"; +import { buildGrokBillingCardRows } from "@/shared/utils/grokBilling"; +import { buildKimiBillingCardRows } from "@/shared/utils/kimiBilling"; +import { + isKimiBillingStatus, + isProviderBillingProvider, + type ProviderBillingStatus, +} from "@/shared/utils/providerBilling"; import { formatCountdown, formatQuotaLabel, @@ -27,19 +33,23 @@ const CURRENCY_SYMBOLS: Record = { const DEFAULT_VISIBLE_ROWS = 3; -function GrokBillingDetails({ billing }: { billing: GrokBillingStatus }) { +function ProviderBillingDetails({ billing }: { billing: ProviderBillingStatus }) { const t = useTranslations("usage"); const locale = useLocale(); - const rows = buildGrokBillingCardRows(billing, locale, (key, fallback) => - translateUsageOrFallback(t, key, fallback) - ); + const rows = isKimiBillingStatus(billing) + ? buildKimiBillingCardRows(billing, locale, (key, fallback) => + translateUsageOrFallback(t, key, fallback) + ) + : buildGrokBillingCardRows(billing, locale, (key, fallback) => + translateUsageOrFallback(t, key, fallback) + ); return (
{rows.map((row) => row.kind === "link" ? ( ) : (
void; @@ -357,7 +367,9 @@ export default function QuotaCardExpanded({
)} - {providerId === "grok-cli" && billing && } + {isProviderBillingProvider(providerId) && billing && ( + + )} {hiddenQuotaRows.length > 0 && (
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts index 3aea687442..63977b1d12 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts @@ -10,15 +10,16 @@ const CODEX_QUOTA_ORDER: Record = { banked_reset_credits: 4, }; const GLM_FAMILY_PROVIDERS = ["glm", "glm-cn", "glmt", "opencode-go"]; +const KIMI_CODING_PROVIDERS = ["kimi-coding", "kimi-coding-apikey"]; /** - * Providers whose quotas already get a deterministic fixed-window order from - * sortGlmOrder()/sortCodexOrder() below. Display layers (e.g. QuotaCardExpanded) + * Providers whose quotas already get a deterministic fixed-window order below + * (Codex, GLM family, and Kimi Coding). Display layers (e.g. QuotaCardExpanded) * must not re-sort these by remaining percentage, or they undo this order (#6687). */ export function hasFixedQuotaOrder(providerId: string | undefined): boolean { const id = String(providerId || "").toLowerCase(); - return id === "codex" || GLM_FAMILY_PROVIDERS.includes(id); + return id === "codex" || GLM_FAMILY_PROVIDERS.includes(id) || KIMI_CODING_PROVIDERS.includes(id); } function quotaEntries(data: any): Array<[string, any]> { @@ -269,6 +270,19 @@ function sortCodexOrder(providerId: string, quotas: any[]) { quotas.sort((a, b) => (CODEX_QUOTA_ORDER[a.name] ?? 99) - (CODEX_QUOTA_ORDER[b.name] ?? 99)); } +function sortKimiOrder(providerId: string, quotas: any[]) { + if (!KIMI_CODING_PROVIDERS.includes(providerId)) return; + const rank = (name: string) => { + if (/^code_5h(?:_|$)/.test(name)) return 0; + if (/^code_7d(?:_|$)/.test(name)) return 1; + return 99; + }; + quotas.sort((a, b) => { + const rankDiff = rank(String(a.name)) - rank(String(b.name)); + return rankDiff || String(a.name).localeCompare(String(b.name)); + }); +} + export function parseQuotaData(provider: string | undefined, data: any) { if (!data || typeof data !== "object") return []; const providerId = String(provider || "").toLowerCase(); @@ -278,6 +292,7 @@ export function parseQuotaData(provider: string | undefined, data: any) { sortProviderModelOrder(provider, normalizedQuotas); sortGlmOrder(providerId, normalizedQuotas); sortCodexOrder(providerId, normalizedQuotas); + sortKimiOrder(providerId, normalizedQuotas); return normalizedQuotas; } catch (error) { console.error(`Error parsing quota data for ${provider}:`, error); diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index add21b4e17..3af32da285 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -392,8 +392,8 @@ const STATUS_ORDER: Record<"critical" | "alert" | "ok", number> = { export function topQuotas(quotas: any[], n = 3, providerId?: string): any[] { const filtered = quotas.filter(Boolean); - // Providers with a deterministic fixed-window order (codex, glm family — see - // quotaParsing.ts's sortCodexOrder()/sortGlmOrder()) must keep the order + // Providers with a deterministic fixed-window order (Codex, GLM family, + // Kimi Coding — see quotaParsing.ts) must keep the order // parseQuotaData() already established rather than being re-sorted by // status/remaining-%, which would undo it (#6687's collapsed-card sibling, #7764). if (hasFixedQuotaOrder(providerId)) { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index f8683269c4..4a67320eab 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -9098,6 +9098,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "month", "grokAdditionalCredits": "Additional Credits", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 0d79c2a0d2..ef36e19471 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -9090,6 +9090,16 @@ "grokAutoTopUpMax": "máximo", "grokAutoTopUpMonth": "mês", "grokAdditionalCredits": "Créditos adicionais", + "kimiExtraUsageCredits": "Créditos de uso extra", + "kimiExtraUsage": "Uso extra", + "kimiExtraUsageEnabled": "Ativado", + "kimiExtraUsageDisabled": "Desativado", + "kimiExtraUsageFrozen": "Congelado", + "kimiExtraUsageUnavailable": "Indisponível", + "kimiMonthlyUsed": "Usado neste mês", + "kimiMonthlyLimit": "Limite mensal", + "kimiMonthlyLimitUnlimited": "Ilimitado", + "kimiAdditionalCredits": "Créditos adicionais", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Gerenciamento de Orçamento", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 16dece1256..1691040f80 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -9098,6 +9098,16 @@ "grokAutoTopUpMax": "tối đa", "grokAutoTopUpMonth": "tháng", "grokAdditionalCredits": "Tín dụng bổ sung", + "kimiExtraUsageCredits": "Tín dụng sử dụng bổ sung", + "kimiExtraUsage": "Sử dụng bổ sung", + "kimiExtraUsageEnabled": "Đã bật", + "kimiExtraUsageDisabled": "Đã tắt", + "kimiExtraUsageFrozen": "Đã đóng băng", + "kimiExtraUsageUnavailable": "Không khả dụng", + "kimiMonthlyUsed": "Đã dùng trong tháng này", + "kimiMonthlyLimit": "Giới hạn hàng tháng", + "kimiMonthlyLimitUnlimited": "Không giới hạn", + "kimiAdditionalCredits": "Tín dụng bổ sung", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Quản lý ngân sách", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index f4b62520a6..18a1ea316f 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -9077,6 +9077,16 @@ "grokAutoTopUpMax": "最大", "grokAutoTopUpMonth": "月", "grokAdditionalCredits": "额外的致谢", + "kimiExtraUsageCredits": "加油包余额", + "kimiExtraUsage": "额度加油包", + "kimiExtraUsageEnabled": "已开启", + "kimiExtraUsageDisabled": "已关闭", + "kimiExtraUsageFrozen": "已冻结", + "kimiExtraUsageUnavailable": "不可用", + "kimiMonthlyUsed": "本月已用", + "kimiMonthlyLimit": "每月限额", + "kimiMonthlyLimitUnlimited": "无限制", + "kimiAdditionalCredits": "充值加油包", "loggerTab": "记录器", "proxyTab": "代理", "budgetManagement": "预算管理", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index a7ac344ae8..7465d3e9f6 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -9077,6 +9077,16 @@ "grokAutoTopUpMax": "最大", "grokAutoTopUpMonth": "月份", "grokAdditionalCredits": "額外的致謝", + "kimiExtraUsageCredits": "加油包餘額", + "kimiExtraUsage": "額度加油包", + "kimiExtraUsageEnabled": "已開啟", + "kimiExtraUsageDisabled": "已關閉", + "kimiExtraUsageFrozen": "已凍結", + "kimiExtraUsageUnavailable": "無法使用", + "kimiMonthlyUsed": "本月已用", + "kimiMonthlyLimit": "每月限額", + "kimiMonthlyLimitUnlimited": "無限制", + "kimiAdditionalCredits": "儲值加油包", "loggerTab": "記錄器", "proxyTab": "代理", "budgetManagement": "預算管理", diff --git a/src/lib/db/providerLimits.ts b/src/lib/db/providerLimits.ts index 427cc4a1ef..677c067a6f 100644 --- a/src/lib/db/providerLimits.ts +++ b/src/lib/db/providerLimits.ts @@ -1,4 +1,7 @@ -import { sanitizeGrokBillingStatus, type GrokBillingStatus } from "@/shared/utils/grokBilling"; +import { + sanitizeProviderBillingStatus, + type ProviderBillingStatus, +} from "@/shared/utils/providerBilling"; import { getDbInstance, isBuildPhase, isCloud } from "./core"; type JsonRecord = Record; @@ -26,7 +29,7 @@ export interface ProviderLimitsCacheEntry { fetchedAt: string; source?: string | null; bankedResetCredits?: number; - billing?: GrokBillingStatus; + billing?: ProviderBillingStatus; } const PROVIDER_LIMITS_CACHE_NAMESPACE = "providerLimitsCache"; @@ -45,7 +48,7 @@ function toRecord(value: unknown): JsonRecord | null { function sanitizeCacheEntryForStorage(entry: ProviderLimitsCacheEntry): ProviderLimitsCacheEntry { const { billing: rawBilling, ...rest } = entry; - const billing = sanitizeGrokBillingStatus(rawBilling); + const billing = sanitizeProviderBillingStatus(rawBilling); return billing ? { ...rest, billing } : rest; } @@ -58,7 +61,7 @@ function normalizeCacheEntry(value: unknown): ProviderLimitsCacheEntry | null { if (!fetchedAt) return null; const bankedResetCredits = Number(record.bankedResetCredits); - const billing = sanitizeGrokBillingStatus(record.billing); + const billing = sanitizeProviderBillingStatus(record.billing); return { quotas: toRecord(record.quotas), diff --git a/src/lib/usage/providerLimitsCache.ts b/src/lib/usage/providerLimitsCache.ts index 75fd031057..6548bef54b 100644 --- a/src/lib/usage/providerLimitsCache.ts +++ b/src/lib/usage/providerLimitsCache.ts @@ -1,5 +1,6 @@ import type { ProviderLimitsCacheEntry } from "@/lib/db/providerLimits"; -import { sanitizeGrokBillingStatus } from "@/shared/utils/grokBilling"; +import { sanitizeProviderBillingStatus } from "@/shared/utils/providerBilling"; +import { GROK_BUILD_ADDITIONAL_CREDITS_URL } from "@/shared/utils/grokBilling"; const GROK_CLI_PROVIDER = "grok-cli"; @@ -26,7 +27,7 @@ export function toProviderLimitsCacheEntry( fetchedAt, source, bankedResetCredits: Number.isFinite(bankedResetCredits) ? bankedResetCredits : undefined, - billing: sanitizeGrokBillingStatus(usage.billing), + billing: sanitizeProviderBillingStatus(usage.billing), }; } @@ -44,14 +45,21 @@ export function mergeProviderLimitsCacheEntry( if (provider !== GROK_CLI_PROVIDER) return next; const nextBilling = next.billing; - const previousAutoTopUp = previous.billing?.autoTopUp; - if (!nextBilling || nextBilling.autoTopUp.available || !previousAutoTopUp) return next; + const previousBilling = previous.billing; + if ( + !nextBilling || + nextBilling.additionalCreditsUrl !== GROK_BUILD_ADDITIONAL_CREDITS_URL || + nextBilling.autoTopUp.available || + !previousBilling || + previousBilling.additionalCreditsUrl !== GROK_BUILD_ADDITIONAL_CREDITS_URL + ) + return next; return { ...next, billing: { ...nextBilling, - autoTopUp: previousAutoTopUp, + autoTopUp: previousBilling.autoTopUp, }, }; } diff --git a/src/shared/utils/kimiBilling.ts b/src/shared/utils/kimiBilling.ts new file mode 100644 index 0000000000..c818a675aa --- /dev/null +++ b/src/shared/utils/kimiBilling.ts @@ -0,0 +1,199 @@ +/** + * Public Dashboard contract for Kimi Coding Extra Usage (额度加油包). + * + * The existing read-only `GET /coding/v1/usages` response carries both the + * Code quota windows and `boosterWallet`. Only the strictly whitelisted fields + * below may cross the Provider Limits cache/UI boundary. + */ + +export const KIMI_CODE_ADDITIONAL_CREDITS_URL = + "https://www.kimi.com/membership/subscription?tab=quota&aff=omniroute"; + +type KimiExtraUsageStatus = "enabled" | "disabled" | "frozen" | "unavailable"; + +export interface KimiBillingStatus { + /** ISO 4217 currency reported by the wallet money wrappers. */ + currency: string; + /** Remaining Extra Usage balance in cents. */ + extraCreditsMinorUnits?: number; + /** Extra Usage spend so far this calendar month, in cents. */ + monthlyUsedMinorUnits?: number; + /** Whether the member enabled a monthly spending cap. */ + monthlyLimitEnabled?: boolean; + /** Monthly spending cap in cents; 0/absent means unlimited. */ + monthlyLimitMinorUnits?: number; + extraUsageStatus: KimiExtraUsageStatus; + additionalCreditsUrl: typeof KIMI_CODE_ADDITIONAL_CREDITS_URL; +} + +type KimiBillingTranslationKey = + | "kimiExtraUsageCredits" + | "kimiExtraUsage" + | "kimiExtraUsageEnabled" + | "kimiExtraUsageDisabled" + | "kimiExtraUsageFrozen" + | "kimiExtraUsageUnavailable" + | "kimiMonthlyUsed" + | "kimiMonthlyLimit" + | "kimiMonthlyLimitUnlimited" + | "kimiAdditionalCredits"; + +type KimiBillingTranslator = (key: KimiBillingTranslationKey, fallback: string) => string; + +type KimiBillingCardRow = + | { kind: "balance" | "status"; label: string; value: string } + | { + kind: "link"; + label: string; + href: typeof KIMI_CODE_ADDITIONAL_CREDITS_URL; + target: "_blank"; + rel: "noreferrer noopener"; + }; + +type JsonRecord = Record; + +function toRecord(value: unknown): JsonRecord | null { + return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null; +} + +function minorUnits(value: unknown): number | undefined { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined; +} + +const ISO_4217 = /^[A-Za-z]{3}$/; +const EXTRA_USAGE_STATUSES = new Set([ + "enabled", + "disabled", + "frozen", + "unavailable", +]); + +export function sanitizeKimiBillingStatus(value: unknown): KimiBillingStatus | undefined { + const billing = toRecord(value); + if (!billing || billing.additionalCreditsUrl !== KIMI_CODE_ADDITIONAL_CREDITS_URL) + return undefined; + + const currency = + typeof billing.currency === "string" && ISO_4217.test(billing.currency) + ? billing.currency.toUpperCase() + : undefined; + const extraUsageStatus = + typeof billing.extraUsageStatus === "string" && + EXTRA_USAGE_STATUSES.has(billing.extraUsageStatus as KimiExtraUsageStatus) + ? (billing.extraUsageStatus as KimiExtraUsageStatus) + : undefined; + if (!currency || !extraUsageStatus) return undefined; + + const extraCreditsMinorUnits = minorUnits(billing.extraCreditsMinorUnits); + const monthlyUsedMinorUnits = minorUnits(billing.monthlyUsedMinorUnits); + const monthlyLimitMinorUnits = minorUnits(billing.monthlyLimitMinorUnits); + const monthlyLimitEnabled = + typeof billing.monthlyLimitEnabled === "boolean" ? billing.monthlyLimitEnabled : undefined; + + return { + currency, + ...(extraCreditsMinorUnits !== undefined ? { extraCreditsMinorUnits } : {}), + ...(monthlyUsedMinorUnits !== undefined ? { monthlyUsedMinorUnits } : {}), + ...(monthlyLimitEnabled !== undefined ? { monthlyLimitEnabled } : {}), + ...(monthlyLimitMinorUnits !== undefined ? { monthlyLimitMinorUnits } : {}), + extraUsageStatus, + additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL, + }; +} + +function formatKimiMinorUnits( + value: number | undefined, + currency: KimiBillingStatus["currency"], + locales?: Intl.LocalesArgument +): string | null { + if (value === undefined) return null; + return new Intl.NumberFormat(locales, { + style: "currency", + currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value / 100); +} + +const fallbackTranslation: KimiBillingTranslator = (_key, fallback) => fallback; + +function formatExtraUsageStatus( + status: KimiExtraUsageStatus, + translate: KimiBillingTranslator +): string { + switch (status) { + case "enabled": + return translate("kimiExtraUsageEnabled", "Enabled"); + case "disabled": + return translate("kimiExtraUsageDisabled", "Disabled"); + case "frozen": + return translate("kimiExtraUsageFrozen", "Frozen"); + default: + return translate("kimiExtraUsageUnavailable", "Unavailable"); + } +} + +export function buildKimiBillingCardRows( + billing: KimiBillingStatus, + locales?: Intl.LocalesArgument, + translate: KimiBillingTranslator = fallbackTranslation +): KimiBillingCardRow[] { + const rows: KimiBillingCardRow[] = []; + const walletPresent = billing.extraCreditsMinorUnits !== undefined; + + const extraCredits = formatKimiMinorUnits( + billing.extraCreditsMinorUnits, + billing.currency, + locales + ); + if (extraCredits !== null) { + rows.push({ + kind: "balance", + label: translate("kimiExtraUsageCredits", "Extra Usage Credits"), + value: extraCredits, + }); + } + + rows.push({ + kind: "status", + label: translate("kimiExtraUsage", "Extra Usage"), + value: formatExtraUsageStatus(billing.extraUsageStatus, translate), + }); + + if (walletPresent) { + const monthlyUsed = formatKimiMinorUnits( + billing.monthlyUsedMinorUnits, + billing.currency, + locales + ); + if (monthlyUsed !== null) { + rows.push({ + kind: "status", + label: translate("kimiMonthlyUsed", "Used this month"), + value: monthlyUsed, + }); + } + + const capped = + billing.monthlyLimitEnabled === true && + billing.monthlyLimitMinorUnits !== undefined && + billing.monthlyLimitMinorUnits > 0; + const monthlyLimit = capped + ? formatKimiMinorUnits(billing.monthlyLimitMinorUnits, billing.currency, locales) + : null; + rows.push({ + kind: "status", + label: translate("kimiMonthlyLimit", "Monthly limit"), + value: monthlyLimit ?? translate("kimiMonthlyLimitUnlimited", "Unlimited"), + }); + } + + rows.push({ + kind: "link", + label: translate("kimiAdditionalCredits", "Additional Credits"), + href: billing.additionalCreditsUrl, + target: "_blank", + rel: "noreferrer noopener", + }); + return rows; +} diff --git a/src/shared/utils/providerBilling.ts b/src/shared/utils/providerBilling.ts new file mode 100644 index 0000000000..f2ddca4dee --- /dev/null +++ b/src/shared/utils/providerBilling.ts @@ -0,0 +1,36 @@ +import { + GROK_BUILD_ADDITIONAL_CREDITS_URL, + sanitizeGrokBillingStatus, + type GrokBillingStatus, +} from "./grokBilling"; +import { + KIMI_CODE_ADDITIONAL_CREDITS_URL, + sanitizeKimiBillingStatus, + type KimiBillingStatus, +} from "./kimiBilling"; + +export type ProviderBillingStatus = GrokBillingStatus | KimiBillingStatus; + +export const PROVIDER_BILLING_PROVIDERS = [ + "grok-cli", + "kimi-coding", + "kimi-coding-apikey", +] as const; + +export function isProviderBillingProvider(provider: string | undefined): boolean { + return ( + provider !== undefined && (PROVIDER_BILLING_PROVIDERS as readonly string[]).includes(provider) + ); +} + +export function sanitizeProviderBillingStatus(value: unknown): ProviderBillingStatus | undefined { + return sanitizeGrokBillingStatus(value) ?? sanitizeKimiBillingStatus(value); +} + +export function isGrokBillingStatus(billing: ProviderBillingStatus): billing is GrokBillingStatus { + return billing.additionalCreditsUrl === GROK_BUILD_ADDITIONAL_CREDITS_URL; +} + +export function isKimiBillingStatus(billing: ProviderBillingStatus): billing is KimiBillingStatus { + return billing.additionalCreditsUrl === KIMI_CODE_ADDITIONAL_CREDITS_URL; +} diff --git a/tests/unit/kimi-coding-billing-ui.test.ts b/tests/unit/kimi-coding-billing-ui.test.ts new file mode 100644 index 0000000000..985ba05205 --- /dev/null +++ b/tests/unit/kimi-coding-billing-ui.test.ts @@ -0,0 +1,159 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { buildKimiBillingCardRows, KIMI_CODE_ADDITIONAL_CREDITS_URL, sanitizeKimiBillingStatus } = + await import("../../src/shared/utils/kimiBilling.ts"); +const { isKimiBillingStatus, isProviderBillingProvider, sanitizeProviderBillingStatus } = + await import("../../src/shared/utils/providerBilling.ts"); +const { PROVIDER_LABEL } = + await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/constants.ts"); +const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); + +const baseBilling = { + currency: "CNY", + extraUsageStatus: "unavailable" as const, + additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL, +}; + +test("Kimi billing rows show the real Extra Usage status when the wallet is unavailable", () => { + const rows = buildKimiBillingCardRows(baseBilling, "en-US"); + assert.deepEqual(rows, [ + { kind: "status", label: "Extra Usage", value: "Unavailable" }, + { + kind: "link", + label: "Additional Credits", + href: KIMI_CODE_ADDITIONAL_CREDITS_URL, + target: "_blank", + rel: "noreferrer noopener", + }, + ]); +}); + +test("Kimi billing rows show balance, wallet status, monthly spend, cap and buy link", () => { + const rows = buildKimiBillingCardRows( + { + ...baseBilling, + extraCreditsMinorUnits: 1234, + monthlyUsedMinorUnits: 250, + monthlyLimitEnabled: true, + monthlyLimitMinorUnits: 5000, + extraUsageStatus: "enabled", + }, + "en-US" + ); + + assert.deepEqual(rows, [ + { kind: "balance", label: "Extra Usage Credits", value: "CN¥12.34" }, + { kind: "status", label: "Extra Usage", value: "Enabled" }, + { kind: "status", label: "Used this month", value: "CN¥2.50" }, + { kind: "status", label: "Monthly limit", value: "CN¥50.00" }, + { + kind: "link", + label: "Additional Credits", + href: KIMI_CODE_ADDITIONAL_CREDITS_URL, + target: "_blank", + rel: "noreferrer noopener", + }, + ]); +}); + +test("Kimi monthly cap displays Unlimited when disabled or zero", () => { + for (const billing of [ + { ...baseBilling, extraCreditsMinorUnits: 0, monthlyLimitEnabled: false }, + { + ...baseBilling, + extraCreditsMinorUnits: 0, + monthlyLimitEnabled: true, + monthlyLimitMinorUnits: 0, + }, + ]) { + const row = buildKimiBillingCardRows(billing, "en-US").find( + (candidate) => candidate.kind === "status" && candidate.label === "Monthly limit" + ); + assert.deepEqual(row, { kind: "status", label: "Monthly limit", value: "Unlimited" }); + } +}); + +test("Kimi billing labels support localized translation fallbacks", () => { + const translate = (key: string, fallback: string) => + ({ + kimiExtraUsageCredits: "加油包余额", + kimiExtraUsage: "额度加油包", + kimiExtraUsageEnabled: "已开启", + kimiExtraUsageDisabled: "已关闭", + kimiExtraUsageFrozen: "已冻结", + kimiExtraUsageUnavailable: "不可用", + kimiMonthlyUsed: "本月已用", + kimiMonthlyLimit: "每月限额", + kimiMonthlyLimitUnlimited: "无限制", + kimiAdditionalCredits: "充值加油包", + })[key] ?? fallback; + + assert.deepEqual( + buildKimiBillingCardRows( + { + ...baseBilling, + extraCreditsMinorUnits: 0, + monthlyLimitEnabled: false, + extraUsageStatus: "disabled", + }, + "zh-CN", + translate + ), + [ + { kind: "balance", label: "加油包余额", value: "¥0.00" }, + { kind: "status", label: "额度加油包", value: "已关闭" }, + { kind: "status", label: "每月限额", value: "无限制" }, + { + kind: "link", + label: "充值加油包", + href: KIMI_CODE_ADDITIONAL_CREDITS_URL, + target: "_blank", + rel: "noreferrer noopener", + }, + ] + ); +}); + +test("Kimi billing sanitizer strips private fields and rejects forged public contracts", () => { + const billing = sanitizeKimiBillingStatus({ + currency: "cny", + extraCreditsMinorUnits: 0, + monthlyUsedMinorUnits: 250, + monthlyLimitEnabled: true, + monthlyLimitMinorUnits: 5000, + extraUsageStatus: "disabled", + paymentMethodId: "secret", + additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL, + rawBody: "secret", + }); + + assert.deepEqual(billing, { + currency: "CNY", + extraCreditsMinorUnits: 0, + monthlyUsedMinorUnits: 250, + monthlyLimitEnabled: true, + monthlyLimitMinorUnits: 5000, + extraUsageStatus: "disabled", + additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL, + }); + assert.equal(buildKimiBillingCardRows(billing!, "zh-CN")[0]?.value, "¥0.00"); + assert.equal(isKimiBillingStatus(billing!), true); + assert.deepEqual(sanitizeProviderBillingStatus(billing), billing); + + for (const forged of [ + { ...baseBilling, currency: "US', - "text/html" - ); - } - - if (u.includes("/api/auth/session")) { - return mockResponse(200, { - accessToken: "jwt-test", - expires: new Date(Date.now() + 3_600_000).toISOString(), - user: { id: "user-test" }, - }); - } - - if (u.includes("/backend-api/settings/user_last_used_model_config")) { - calls.userConfigUrl = u; - calls.userConfigMethod = (opts.method || "GET").toUpperCase(); - return mockResponse(200, { is_disabled: false }); - } - - if (u.includes("/backend-api/sentinel/chat-requirements")) { - return mockResponse(200, { - token: "requirements-test", - proofofwork: { required: false }, - }); - } - - if (u.endsWith("/backend-api/f/conversation")) { - calls.conversationBody = opts.body ?? null; - return mockResponse( - 200, - [ - `data: ${JSON.stringify({ - conversation_id: "conv-test", - message: { - id: "msg-test", - author: { role: "assistant" }, - content: { content_type: "text", parts: ["ok"] }, - status: "finished_successfully", - }, - })}`, - "", - "data: [DONE]", - "", - ].join("\r\n"), - "text/event-stream" - ); - } - - // Browser-like warmup endpoints are best-effort. Returning a normal 200 - // keeps this focused test independent from their response details. - return mockResponse(200, {}); - }); - - return { - calls, - restore() { - __setTlsFetchOverrideForTesting(null); - }, - }; -} - -test("ChatGPT Web thinking effort aliases map to the three native tiers", () => { +test("ChatGPT Web performance lanes use their native model and effort pairs", () => { const cases = [ - ["minimal", "standard"], - ["low", "standard"], - ["medium", "standard"], - ["standard", "standard"], - ["high", "extended"], - ["extended", "extended"], - ["xhigh", "max"], - ["max", "max"], + ["gpt-5.6-luna-free", "auto", null, false], + ["gpt-5.6-luna-free-thinking", "auto", null, false], + ["gpt-5.6-sol-instant", "gpt-5-6", null, false], + ["gpt-5.6-sol-medium", "gpt-5-6-thinking", "standard", false], + ["gpt-5.6-sol-high", "gpt-5-6-thinking", "extended", false], + ["gpt-5.6-sol-xhigh", "gpt-5-6-thinking", "max", false], + ["gpt-5.6-sol-pro", "gpt-5-6-pro", "standard", true], + ["gpt-5.5-instant", "gpt-5-5", null, false], + ["gpt-5.5-medium", "gpt-5-5-thinking", "standard", false], + ["gpt-5.5-high", "gpt-5-5-thinking", "extended", false], + ["gpt-5.5-xhigh", "gpt-5-5-thinking", "max", false], + ["gpt-5.5-pro", "gpt-5-5-pro", "standard", true], + ["gpt-5.5-pro-extended", "gpt-5-5-pro", "extended", true], ] as const; - for (const [input, expected] of cases) { - assert.equal(normalizeThinkingEffort(input), expected, input); + for (const [model, slug, effort, isPro] of cases) { + assert.deepEqual(resolveChatGptModel(model), { slug, effort, isPro }, model); } }); -test("providerSpecificData can request native max with highest precedence", () => { - const resolved = resolveChatGptModel( - "gpt-5.6-thinking", - { reasoning_effort: "low" }, - { thinkingEffort: "max" } - ); - assert.equal(resolved.effort, "max"); +test("ChatGPT Web Free Luna Think uses the captured reason system hint", () => { + assert.deepEqual(resolveChatGptSystemHints("gpt-5.6-luna-free"), []); + assert.deepEqual(resolveChatGptSystemHints("gpt-5.6-luna-free-thinking"), ["reason"]); }); - -for (const effort of ["xhigh", "max"] as const) { - test(`ChatGPT Web executor sends ${effort} as thinking_effort=max`, async () => { - __resetChatGptWebCachesForTesting(); - const mock = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - const result = await executor.execute({ - model: "gpt-5.6-thinking", - body: { - messages: [{ role: "user", content: "hi" }], - reasoning_effort: effort, - }, - stream: false, - credentials: { apiKey: `cookie-${effort}` }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - - assert.equal(result.response.status, 200); - assert.equal(mock.calls.userConfigMethod, "PATCH"); - assert.ok(mock.calls.userConfigUrl); - const settingsUrl = new URL(mock.calls.userConfigUrl); - assert.equal(settingsUrl.searchParams.get("model_slug"), "gpt-5-6-thinking"); - assert.equal(settingsUrl.searchParams.get("thinking_effort"), "max"); - - assert.ok(mock.calls.conversationBody); - const conversationBody = JSON.parse(mock.calls.conversationBody) as Record; - assert.equal(conversationBody.thinking_effort, "max"); - } finally { - mock.restore(); - } - }); -} diff --git a/tests/unit/chatgpt-web-models-split.test.ts b/tests/unit/chatgpt-web-models-split.test.ts index f25f78556d..ef9ff79a77 100644 --- a/tests/unit/chatgpt-web-models-split.test.ts +++ b/tests/unit/chatgpt-web-models-split.test.ts @@ -5,16 +5,16 @@ import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; // Split-guard for the chatgpt-web model-mapping extraction. -// The static model maps + pure thinking-effort resolvers live in the pure leaf -// chatgpt-web/models.ts (no module state). Host imports the two it uses back. +// The static model maps + pure model resolver live in the pure leaf +// chatgpt-web/models.ts (no module state). Host imports it back. const HERE = dirname(fileURLToPath(import.meta.url)); const EXE = join(HERE, "../../open-sse/executors"); const HOST = join(EXE, "chatgpt-web.ts"); const LEAF = join(EXE, "chatgpt-web/models.ts"); -test("leaf hosts the model maps + resolvers and does not import the host", () => { +test("leaf hosts the model maps + resolver and does not import the host", () => { const src = readFileSync(LEAF, "utf8"); - for (const sym of ["MODEL_MAP", "resolveChatGptModel", "resolveThinkingEffort"]) { + for (const sym of ["MODEL_MAP", "MODEL_FORCED_EFFORT", "resolveChatGptModel"]) { assert.match(src, new RegExp(`export (const|function) ${sym}\\b`)); } assert.doesNotMatch(src, /from "\.\.\/chatgpt-web\.ts"/); diff --git a/tests/unit/chatgpt-web-tools-5240.test.ts b/tests/unit/chatgpt-web-tools-5240.test.ts index 5fc9ec06c3..ac6bc9046a 100644 --- a/tests/unit/chatgpt-web-tools-5240.test.ts +++ b/tests/unit/chatgpt-web-tools-5240.test.ts @@ -8,15 +8,13 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } = await import( - "../../open-sse/executors/chatgpt-web.ts" -); -const { __setTlsFetchOverrideForTesting } = await import( - "../../open-sse/services/chatgptTlsClient.ts" -); +const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } = + await import("../../open-sse/executors/chatgpt-web.ts"); +const { __setTlsFetchOverrideForTesting } = + await import("../../open-sse/services/chatgptTlsClient.ts"); // ─── Minimal TLS-fetch mock ────────────────────────────────────────────────── -// Tailored to the tool-call flow (gpt-5.3-instant, non-thinking): root/DPL, +// Tailored to the tool-call flow (gpt-5.5, non-thinking): root/DPL, // session→accessToken, sentinel→token (no PoW), conv→SSE. Warmup GETs fall // through to 404, which the executor tolerates. @@ -68,7 +66,10 @@ function installMockFetch(convEvents: unknown[]) { body: null, }); - if ((u === "https://chatgpt.com/" || u === "https://chatgpt.com") && (opts.method || "GET") === "GET") { + if ( + (u === "https://chatgpt.com/" || u === "https://chatgpt.com") && + (opts.method || "GET") === "GET" + ) { return { status: 200, headers: makeHeaders({ "Content-Type": "text/html" }), @@ -127,7 +128,7 @@ const TOOL_CALL_TEXT = '{"name":"get_weather","arguments":{"location":"Tok function baseOpts(extra: Record) { return { - model: "gpt-5.3-instant", + model: "gpt-5.5", credentials: { apiKey: "test" }, signal: AbortSignal.timeout(10_000), log: null, diff --git a/tests/unit/chatgpt-web-tools-7679.test.ts b/tests/unit/chatgpt-web-tools-7679.test.ts index f042f9ab7d..0ce60ba21f 100644 --- a/tests/unit/chatgpt-web-tools-7679.test.ts +++ b/tests/unit/chatgpt-web-tools-7679.test.ts @@ -1,6 +1,6 @@ -// Tool contract serialization for chatgpt-web thinking models (#7679). +// Tool contract serialization for ChatGPT Web performance models (#7679). // -// GPT-5.6 Thinking via chatgpt-web ignores the injected `` pseudo-contract +// GPT-5.6 Sol via chatgpt-web ignores the injected `` pseudo-contract // and replies in prose claiming tools are unavailable. This test covers the // nonce-bound serialization that clearly describes client-side tools and places // the full contract at the tail of the effective message list. @@ -161,7 +161,7 @@ test("parseToolCallsFromText returns null when hardened text has no tool blocks }); test("parseToolCallsFromText handles blocks line-boundary crossing in hardened text (#7679)", () => { - // Some thinking models may emit the tool block adjacent to explanatory text + // Some high-performance lanes may emit the tool block adjacent to explanatory text // with no preceding newline const text = [ 'I will use the weather tool. {"name":"get_weather","arguments":{"location":"Paris"}}', diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index cc97ecd69d..267c0eb6dc 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -81,13 +81,11 @@ type MockFetchOptions = { attachmentDownload?: MockTlsConfig; conversationDetail?: MockTlsConfig | MockTlsConfig[]; signedDownload?: MockTlsConfig; - userConfig?: MockTlsConfig; onSession?: (opts: TlsFetchOptions) => void; onSentinel?: (opts: TlsFetchOptions) => void; onConv?: (opts: TlsFetchOptions) => void; onFileDownload?: (opts: TlsFetchOptions, fileId: string) => void; onAttachmentDownload?: (opts: TlsFetchOptions, fileId: string) => void; - onUserConfig?: (opts: TlsFetchOptions, url: string) => void; }; type MockFetchCalls = { @@ -99,9 +97,6 @@ type MockFetchCalls = { attachmentDownload: number; conversationDetail: number; signedDownload: number; - userConfig: number; - userConfigUrls: string[]; - userConfigMethods: string[]; urls: string[]; headers: Array | undefined>; bodies: Array; @@ -118,13 +113,11 @@ function installMockFetch({ attachmentDownload, conversationDetail, signedDownload, - userConfig, onSession, onSentinel, onConv, onFileDownload, onAttachmentDownload, - onUserConfig, }: MockFetchOptions = {}) { const calls: MockFetchCalls = { session: 0, @@ -135,9 +128,6 @@ function installMockFetch({ attachmentDownload: 0, conversationDetail: 0, signedDownload: 0, - userConfig: 0, - userConfigUrls: [], - userConfigMethods: [], urls: [], headers: [], bodies: [], @@ -188,22 +178,6 @@ function installMockFetch({ }; } - // /backend-api/settings/user_last_used_model_config?model_slug=...&thinking_effort=... - // Match before sentinel since /settings/* is its own surface. - if (u.includes("/backend-api/settings/user_last_used_model_config")) { - calls.userConfig++; - calls.userConfigUrls.push(u); - calls.userConfigMethods.push((opts.method || "GET").toUpperCase()); - if (onUserConfig) onUserConfig(opts, u); - const cfg = userConfig ?? { status: 200, body: { is_disabled: false } }; - return { - status: cfg.status, - headers: makeHeaders({ "Content-Type": "application/json" }), - text: typeof cfg.body === "string" ? cfg.body : JSON.stringify(cfg.body || {}), - body: null, - }; - } - if (u.includes("/sentinel/chat-requirements")) { calls.sentinel++; if (onSentinel) onSentinel(opts); @@ -288,7 +262,7 @@ function installMockFetch({ }; } - // /backend-api/conversation/ — detail poll used by GPT-5.5 Pro handoff. + // /backend-api/conversation/ — detail poll used by GPT-5.6 Sol Pro handoff. { const m1 = u.match(/\/backend-api\/conversation\/([^/?#]+)$/); if (m1) { @@ -480,7 +454,7 @@ test("Token exchange: cookie sent to /api/auth/session, accessToken used as Bear try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "my-cookie-value" }, @@ -518,7 +492,7 @@ test("Token cache: two calls within TTL only hit /api/auth/session once", async try { const executor = new ChatGptWebExecutor(); const opts = { - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "cookie-v1" }, @@ -552,7 +526,7 @@ test("Refreshed cookie: surfaced via onCredentialsRefreshed callback", async () let refreshed = null; const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "old-cookie" }, @@ -585,7 +559,7 @@ test("Sentinel: chat-requirements is hit before /backend-api/conversation", asyn try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -606,7 +580,7 @@ test("Sentinel: chat-requirements token forwarded on conv request", async () => try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -635,7 +609,7 @@ test("PoW: when required, proof token is sent with valid prefix", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -670,7 +644,7 @@ test("Turnstile: required flag does NOT block — conv endpoint accepts requests try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -692,7 +666,7 @@ test("Non-streaming: returns OpenAI chat.completion JSON", async () => { try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -743,7 +717,7 @@ test("Streaming: produces valid SSE chunks ending with [DONE]", async () => { try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }], stream: true }, stream: true, credentials: { apiKey: "test" }, @@ -807,7 +781,7 @@ test("Streaming: cumulative parts are diffed into non-overlapping deltas", async try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }], stream: true }, stream: true, credentials: { apiKey: "test" }, @@ -835,7 +809,7 @@ test("Streaming: cumulative parts are diffed into non-overlapping deltas", async } }); -test("GPT-5.5 Pro streaming: preserves interim reasoning and appends final polled answer", async () => { +test("GPT-5.6 Sol Pro streaming: preserves interim reasoning and appends final polled answer", async () => { reset(); const m = installMockFetch({ conv: { @@ -878,7 +852,7 @@ test("GPT-5.5 Pro streaming: preserves interim reasoning and appends final polle try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.5-pro-extended", + model: "gpt-5.6-sol-pro", body: { messages: [{ role: "user", content: "hard problem" }], stream: true }, stream: true, credentials: { apiKey: "cookie-pro-stream" }, @@ -907,7 +881,7 @@ test("Error: 401 on /api/auth/session returns 401 with re-paste hint", async () try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "expired-cookie" }, @@ -928,7 +902,7 @@ test("Error: 200 with no accessToken returns 401", async () => { try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "stale-cookie" }, @@ -948,7 +922,7 @@ test("Error: 403 from sentinel returns 403 SENTINEL_BLOCKED", async () => { try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -970,7 +944,7 @@ test("Error: 429 from conversation returns 429 with rate-limit message", async ( try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -991,7 +965,7 @@ test("Error: empty messages returns 400 without any fetch", async () => { try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [] }, stream: false, credentials: { apiKey: "test" }, @@ -1011,7 +985,7 @@ test("Error: missing apiKey returns 401 without any fetch", async () => { try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: {}, @@ -1033,7 +1007,7 @@ test("Cookie: bare value gets prepended with cookie name", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "rawValue" }, @@ -1052,7 +1026,7 @@ test("Cookie: unchunked cookie line is passed through verbatim", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "__Secure-next-auth.session-token=actualvalue" }, @@ -1071,7 +1045,7 @@ test("Cookie: chunked .0/.1 cookies are passed through verbatim (NextAuth reasse try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { @@ -1096,7 +1070,7 @@ test("Cookie: 'Cookie: ' DevTools prefix is stripped", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { @@ -1127,7 +1101,7 @@ test("Session continuity: each call starts a fresh conversation (Temporary Chat try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "First question" }] }, stream: false, credentials: { apiKey: "test" }, @@ -1135,7 +1109,7 @@ test("Session continuity: each call starts a fresh conversation (Temporary Chat log: null, }); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [ { role: "user", content: "First question" }, @@ -1178,7 +1152,7 @@ test("Request: conversation POST has correct browser-like headers", async () => try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -1204,7 +1178,7 @@ test("Request: payload has correct ChatGPT shape", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [ { role: "system", content: "Be concise" }, @@ -1219,7 +1193,7 @@ test("Request: payload has correct ChatGPT shape", async () => { const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); const body = JSON.parse(m.calls.bodies[convIdx]); assert.equal(body.action, "next"); - assert.equal(body.model, "gpt-5-3-instant"); + assert.equal(body.model, "gpt-5-5"); // Plain text request → Temporary Chat stays ON. We disable it only for // image-gen prompts (see "Image gen: image-intent prompts" tests below). assert.equal(body.history_and_training_disabled, true); @@ -1245,26 +1219,23 @@ test("Provider registry: chatgpt-web exposes the current ChatGPT Web model catal assert.equal(entry.authHeader, "cookie"); const ids = (entry.models || []).map((m) => m.id); - // Retired GPT-5.4 and older entries stay out of the advertised catalog. + // Free accounts expose Luna with an optional Think toggle; paid accounts + // expose five GPT-5.6 Sol performance lanes plus GPT-5.5. assert.deepEqual(ids, [ - "gpt-5.6-pro", - "gpt-5.6-thinking", + "gpt-5.6-sol-pro", + "gpt-5.6-sol-xhigh", + "gpt-5.6-sol-high", + "gpt-5.6-sol-medium", + "gpt-5.6-sol-instant", + "gpt-5.6-luna-free-thinking", + "gpt-5.6-luna-free", "gpt-5.5-pro-extended", "gpt-5.5-pro", - "gpt-5.5-thinking", - "gpt-5.5", - "o3", + "gpt-5.5-xhigh", + "gpt-5.5-high", + "gpt-5.5-medium", + "gpt-5.5-instant", ]); - assert.equal( - ids.some((id) => id.startsWith("gpt-5.4")), - false - ); - - const { MODEL_MAP } = await import("../../open-sse/executors/chatgpt-web/models.ts"); - assert.equal( - Object.keys(MODEL_MAP).some((id) => id.startsWith("gpt-5.4") || id.startsWith("gpt-5-4")), - false - ); }); test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", async () => { @@ -1273,19 +1244,22 @@ test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", asy try { const cases: Array<[string, string]> = [ // Public catalog ids. - ["gpt-5.6-pro", "gpt-5-6-pro"], - ["gpt-5.6-thinking", "gpt-5-6-thinking"], - ["gpt-5.5-thinking", "gpt-5-5-thinking"], - ["gpt-5.5", "gpt-5-5"], + ["gpt-5.6-luna-free", "auto"], + ["gpt-5.6-luna-free-thinking", "auto"], + ["gpt-5.6-sol-instant", "gpt-5-6"], + ["gpt-5.6-sol-medium", "gpt-5-6-thinking"], + ["gpt-5.6-sol-high", "gpt-5-6-thinking"], + ["gpt-5.6-sol-xhigh", "gpt-5-6-thinking"], + ["gpt-5.6-sol-pro", "gpt-5-6-pro"], + ["gpt-5.5-instant", "gpt-5-5"], + ["gpt-5.5-medium", "gpt-5-5-thinking"], + ["gpt-5.5-high", "gpt-5-5-thinking"], + ["gpt-5.5-xhigh", "gpt-5-5-thinking"], ["gpt-5.5-pro", "gpt-5-5-pro"], ["gpt-5.5-pro-extended", "gpt-5-5-pro"], - ["o3", "o3"], // Backend dash-form slugs are still accepted for direct provider/model callers. - ["gpt-5-3", "gpt-5-3"], - ["gpt-5-5-thinking", "gpt-5-5-thinking"], - ["gpt-5-6-pro", "gpt-5-6-pro"], - ["gpt-5-5-pro", "gpt-5-5-pro"], - ["gpt-5-5-pro-extended", "gpt-5-5-pro"], + ["gpt-5-6", "gpt-5-6"], + ["gpt-5-5", "gpt-5-5"], ]; for (const [omniId, expectedSlug] of cases) { m.calls.urls.length = 0; @@ -1308,18 +1282,58 @@ test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", asy } }); +test("GPT-5.6 Luna Free Think sends the captured auto-router reason hints", async () => { + reset(); + const m = installMockFetch(); + try { + const executor = new ChatGptWebExecutor(); + for (const [model, expectedHints] of [ + ["gpt-5.6-luna-free", undefined], + ["gpt-5.6-luna-free-thinking", ["reason"]], + ] as const) { + m.calls.urls.length = 0; + m.calls.bodies.length = 0; + await executor.execute({ + model, + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: { apiKey: "cookie-free-luna" }, + signal: AbortSignal.timeout(10_000), + log: null, + }); + const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); + const body = JSON.parse(m.calls.bodies[convIdx]); + const userMessage = body.messages.find( + (message: { author?: { role?: string } }) => message.author?.role === "user" + ); + + assert.equal(body.model, "auto"); + assert.deepEqual(body.system_hints, expectedHints); + assert.deepEqual(userMessage?.metadata?.system_hints, expectedHints); + } + } finally { + m.restore(); + } +}); + test("MODEL_MAP drift guard: every advertised catalog id reaches ChatGPT as a backend slug", async () => { reset(); const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); const ids = (getRegistryEntry("chatgpt-web")?.models || []).map((m) => m.id); const expectedSlugById: Record = { - "gpt-5.6-pro": "gpt-5-6-pro", - "gpt-5.6-thinking": "gpt-5-6-thinking", - "gpt-5.5-pro-extended": "gpt-5-5-pro", + "gpt-5.6-luna-free": "auto", + "gpt-5.6-luna-free-thinking": "auto", + "gpt-5.6-sol-instant": "gpt-5-6", + "gpt-5.6-sol-medium": "gpt-5-6-thinking", + "gpt-5.6-sol-high": "gpt-5-6-thinking", + "gpt-5.6-sol-xhigh": "gpt-5-6-thinking", + "gpt-5.6-sol-pro": "gpt-5-6-pro", + "gpt-5.5-instant": "gpt-5-5", + "gpt-5.5-medium": "gpt-5-5-thinking", + "gpt-5.5-high": "gpt-5-5-thinking", + "gpt-5.5-xhigh": "gpt-5-5-thinking", "gpt-5.5-pro": "gpt-5-5-pro", - "gpt-5.5-thinking": "gpt-5-5-thinking", - "gpt-5.5": "gpt-5-5", - o3: "o3", + "gpt-5.5-pro-extended": "gpt-5-5-pro", }; const m = installMockFetch(); try { @@ -1348,15 +1362,15 @@ test("MODEL_MAP drift guard: every advertised catalog id reaches ChatGPT as a ba } }); -// ─── thinking_effort PATCH user_last_used_model_config ───────────────────── +// ─── GPT-5.6 Sol picker request contract ────────────────────────────────── -test("GPT-5.5 Pro Extended sends base slug with extended effort and Temporary Chat", async () => { +test("GPT-5.6 Sol XHigh sends the captured thinking-model/max pair", async () => { reset(); const m = installMockFetch(); try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.5-pro-extended", + model: "gpt-5.6-sol-xhigh", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "cookie-pro-extended" }, @@ -1366,26 +1380,25 @@ test("GPT-5.5 Pro Extended sends base slug with extended effort and Temporary Ch assert.equal(result.response.status, 200); const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); const body = JSON.parse(m.calls.bodies[convIdx]); - assert.equal(body.model, "gpt-5-5-pro"); - assert.equal(body.thinking_effort, "extended"); + assert.equal(body.model, "gpt-5-6-thinking"); + assert.equal(body.thinking_effort, "max"); assert.equal(body.history_and_training_disabled, true); - assert.equal( - m.calls.userConfig, - 0, - "Pro effort is sent with the turn, not PATCHed as a thinking-model preference" + assert.ok( + !m.calls.urls.some((url) => url.includes("/settings/user_last_used_model_config")), + "the captured browser request uses no settings PATCH" ); } finally { m.restore(); } }); -test("GPT-5.5 Pro standard sends standard effort", async () => { +test("GPT-5.6 Sol High sends the captured thinking-model/extended pair", async () => { reset(); const m = installMockFetch(); try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.5-pro", + model: "gpt-5.6-sol-high", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "cookie-pro-standard" }, @@ -1394,21 +1407,21 @@ test("GPT-5.5 Pro standard sends standard effort", async () => { }); const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); const body = JSON.parse(m.calls.bodies[convIdx]); - assert.equal(body.model, "gpt-5-5-pro"); - assert.equal(body.thinking_effort, "standard"); + assert.equal(body.model, "gpt-5-6-thinking"); + assert.equal(body.thinking_effort, "extended"); assert.equal(body.history_and_training_disabled, true); } finally { m.restore(); } }); -test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utility calls", async () => { +test("GPT-5.6 Sol XHigh store:false keeps Temporary Chat enabled", async () => { reset(); const m = installMockFetch(); try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.5-pro-extended", + model: "gpt-5.6-sol-xhigh", body: { store: false, messages: [ @@ -1424,8 +1437,8 @@ test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utilit assert.equal(result.response.status, 200); const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation")); const body = JSON.parse(m.calls.bodies[convIdx]); - assert.equal(body.model, "gpt-5-5-pro"); - assert.equal(body.thinking_effort, "extended"); + assert.equal(body.model, "gpt-5-6-thinking"); + assert.equal(body.thinking_effort, "max"); assert.equal(body.history_and_training_disabled, true); assert.equal( m.calls.conversationDetail, @@ -1437,242 +1450,6 @@ test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utilit } }); -test("thinking_effort: high → PATCH user_last_used_model_config with extended", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: "gpt-5.5-thinking", - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" }, - stream: false, - credentials: { apiKey: "cookie-1" }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 1, "exactly one PATCH issued"); - assert.equal(m.calls.userConfigMethods[0], "PATCH"); - const u = m.calls.userConfigUrls[0]; - assert.match(u, /model_slug=gpt-5-5-thinking/); - assert.match(u, /thinking_effort=extended/); - } finally { - m.restore(); - } -}); - -test("thinking_effort: low/medium → PATCH with standard", async () => { - for (const effort of ["low", "medium", "minimal"]) { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: "gpt-5.6-thinking", - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: effort }, - stream: false, - credentials: { apiKey: `cookie-${effort}` }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 1, `effort=${effort} should issue exactly one PATCH`); - assert.match(m.calls.userConfigUrls[0], /thinking_effort=standard/, `${effort} → standard`); - assert.match(m.calls.userConfigUrls[0], /model_slug=gpt-5-6-thinking/); - } finally { - m.restore(); - } - } -}); - -test("thinking_effort: instant model never triggers PATCH even with reasoning_effort", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: "gpt-5.3-instant", - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" }, - stream: false, - credentials: { apiKey: "cookie-instant" }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 0, "instant slug must not PATCH thinking_effort"); - } finally { - m.restore(); - } -}); - -test("thinking_effort: bare chatgpt.com thinking slugs still PATCH", async () => { - for (const bareSlug of ["gpt-5-6-thinking", "gpt-5-5-thinking", "o3"]) { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: bareSlug, - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" }, - stream: false, - credentials: { apiKey: `cookie-bare-${bareSlug}` }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal( - m.calls.userConfig, - 1, - `bare slug ${bareSlug} must trigger thinking_effort PATCH` - ); - assert.ok( - m.calls.userConfigUrls[0].includes(`model_slug=${bareSlug}`), - `URL should contain model_slug=${bareSlug}` - ); - } finally { - m.restore(); - } - } -}); - -test("thinking_effort: thinking model without reasoning_effort skips PATCH", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: "gpt-5.5-thinking", - body: { messages: [{ role: "user", content: "hi" }] }, - stream: false, - credentials: { apiKey: "cookie-noeffort" }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 0, "no effort requested → no PATCH"); - } finally { - m.restore(); - } -}); - -test("thinking_effort: providerSpecificData.thinkingEffort=extended overrides body", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: "gpt-5.6-thinking", - body: { - messages: [{ role: "user", content: "hi" }], - reasoning_effort: "low", // would normally map to standard - }, - stream: false, - credentials: { - apiKey: "cookie-override", - providerSpecificData: { thinkingEffort: "extended" }, - }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 1); - assert.match(m.calls.userConfigUrls[0], /model_slug=gpt-5-6-thinking/); - assert.match(m.calls.userConfigUrls[0], /thinking_effort=extended/); - } finally { - m.restore(); - } -}); - -test("thinking_effort: nested body.reasoning.effort=high → extended", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - await executor.execute({ - model: "gpt-5.5-thinking", - body: { - messages: [{ role: "user", content: "hi" }], - reasoning: { effort: "high" }, - }, - stream: false, - credentials: { apiKey: "cookie-nested" }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 1); - assert.match(m.calls.userConfigUrls[0], /model_slug=gpt-5-5-thinking/); - assert.match(m.calls.userConfigUrls[0], /thinking_effort=extended/); - } finally { - m.restore(); - } -}); - -test("thinking_effort: cached per (cookie, slug, effort) — second identical call skips PATCH", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - const opts = { - model: "gpt-5.5-thinking", - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" }, - stream: false, - credentials: { apiKey: "cookie-cache" }, - signal: AbortSignal.timeout(10_000), - log: null, - }; - await executor.execute(opts); - await executor.execute(opts); - assert.equal(m.calls.userConfig, 1, "second identical request hits cache"); - } finally { - m.restore(); - } -}); - -test("thinking_effort: switching effort within TTL triggers a fresh PATCH", async () => { - reset(); - const m = installMockFetch(); - try { - const executor = new ChatGptWebExecutor(); - const base = { - model: "gpt-5.5-thinking", - stream: false, - credentials: { apiKey: "cookie-switch" }, - signal: AbortSignal.timeout(10_000), - log: null, - }; - await executor.execute({ - ...base, - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" }, - }); - await executor.execute({ - ...base, - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "low" }, - }); - assert.equal(m.calls.userConfig, 2, "different effort key bypasses cache"); - assert.match(m.calls.userConfigUrls[0], /thinking_effort=extended/); - assert.match(m.calls.userConfigUrls[1], /thinking_effort=standard/); - } finally { - m.restore(); - } -}); - -test("thinking_effort: PATCH failure is non-fatal — conversation request still fires", async () => { - reset(); - const m = installMockFetch({ - userConfig: { status: 500, body: { error: "boom" } }, - }); - try { - const executor = new ChatGptWebExecutor(); - const result = await executor.execute({ - model: "gpt-5.5-thinking", - body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" }, - stream: false, - credentials: { apiKey: "cookie-fail" }, - signal: AbortSignal.timeout(10_000), - log: null, - }); - assert.equal(m.calls.userConfig, 1); - assert.equal(m.calls.conv, 1, "conversation still issued despite settings PATCH 500"); - assert.equal(result.response.status, 200); - } finally { - m.restore(); - } -}); - test("Image registry: cgpt-web/gpt-5.5 routes to ChatGPT Web image handler", async () => { const { parseImageModel, getImageProvider } = await import("../../open-sse/config/imageRegistry.ts"); @@ -1709,7 +1486,7 @@ test("Cookie rotation: full DevTools blob keeps cf_clearance/__cf_bm/_cfuvid", a let refreshed = null; const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { @@ -1764,7 +1541,7 @@ test("Cookie rotation: unchunked → chunked drops stale unchunked variant", asy let refreshed = null; const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { @@ -1811,7 +1588,7 @@ test("Cookie rotation: chunked → unchunked drops stale chunks", async () => { let refreshed = null; const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { @@ -1855,7 +1632,7 @@ test("Cookie rotation: returns null when Set-Cookie has no session-token", async let refreshed = null; const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "cookie-v1" }, @@ -1919,7 +1696,7 @@ test("Stream parser: echoed prior assistant turn is suppressed (streaming)", asy try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }], stream: true }, stream: true, credentials: { apiKey: "test" }, @@ -1979,7 +1756,7 @@ test("Stream parser: echoed prior assistant turn is suppressed (non-streaming)", try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2017,7 +1794,7 @@ test("Stream parser: instant single-event reply still surfaces via fallback", as try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2083,7 +1860,7 @@ test("Error: TlsClientUnavailableError returns 502 with TLS_UNAVAILABLE code", a try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2211,7 +1988,7 @@ test("Image gen: file-service:// pointer resolves to download URL and is appende try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "generate an image of a kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2245,7 +2022,7 @@ test("Image gen: file-service:// pointer is appended in streaming SSE", async () try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "draw a kitten" }] }, stream: true, credentials: { apiKey: "test" }, @@ -2279,7 +2056,7 @@ test("Image gen: sediment:// pointer prefers /files//download over /attachme try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "make a kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2324,7 +2101,7 @@ test("Image gen: failed download URL is dropped silently — no broken markdown" try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2348,7 +2125,7 @@ test("Image gen: image-intent prompt disables Temporary Chat", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "generate an image of a kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2369,7 +2146,7 @@ test("Image gen: text-only prompt keeps Temporary Chat ON", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "what is the capital of France?" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2396,7 +2173,7 @@ test("Image gen: Open WebUI follow-up/title/tag tool prompts do NOT trigger imag try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: prompt }] }, stream: false, credentials: { apiKey: "test" }, @@ -2429,7 +2206,7 @@ test("Image gen: Open WebUI image-generation context suppresses duplicate chat i try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [ { role: "system", content: context }, @@ -2477,7 +2254,7 @@ test("Image gen: heuristic catches common phrasings", async () => { try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: phrase }] }, stream: false, credentials: { apiKey: "test" }, @@ -2598,7 +2375,7 @@ test("Image gen: signed URL bytes are cached and exposed via /v1/chatgpt-web/ima try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "draw kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2648,7 +2425,7 @@ test("Image gen: prior data: image URIs are stripped from history before upstrea const assistantMsg = `Sure, here you go:\n\n![image](data:image/png;base64,${huge})\n`; const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [ { role: "user", content: "draw a kitten" }, @@ -2686,7 +2463,7 @@ test("Image edit: cached OmniRoute image URL continues the saved ChatGPT convers try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [ { role: "user", content: "draw a kitten" }, @@ -2726,7 +2503,7 @@ test("Image edit: Open WebUI image context suppresses duplicate edit continuatio try { const executor = new ChatGptWebExecutor(); await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [ { @@ -2798,7 +2575,7 @@ test("Image gen: dedupes the same pointer across in-progress + finished events", try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2829,7 +2606,7 @@ test("Image gen: bytes-fetch failure drops markdown (no signed-URL fallback)", a try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "draw a kitten" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2903,7 +2680,7 @@ test("Image edit: file_0000XXXX (chatgpt-web edit result) falls back to /convers try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "now make it nighttime" }] }, stream: false, credentials: { apiKey: "test" }, @@ -2965,7 +2742,7 @@ test("Image gen: ChatGPT-internal tool_invoked metadata does NOT spuriously trig try { const executor = new ChatGptWebExecutor(); const result = await executor.execute({ - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { messages: [{ role: "user", content: "limitations of gpt-4o-mini?" }] }, stream: true, credentials: { apiKey: "test" }, @@ -3015,7 +2792,7 @@ test("Image edit handler: bytes-hash match drives executor with cached conversat const { handleImageEdit } = await import("../../open-sse/handlers/imageGeneration.ts"); const result = await handleImageEdit({ provider: "chatgpt-web", - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { prompt: "turn it to day time" }, imageBytes: sourceBytes, credentials: { apiKey: "test" }, @@ -3053,7 +2830,7 @@ test("Image edit handler: no cached match returns 400 (does not silently generat const foreignBytes = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0xde, 0xad, 0xbe, 0xef]); const result = await handleImageEdit({ provider: "chatgpt-web", - model: "gpt-5.3-instant", + model: "gpt-5.5", body: { prompt: "turn it to day time" }, imageBytes: foreignBytes, credentials: { apiKey: "test" }, @@ -3082,7 +2859,7 @@ test("Image gen handler: n>4 is rejected before any upstream call", async () => try { const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts"); const result = await handleImageGeneration({ - body: { prompt: "draw a kitten", n: 5, model: "cgpt-web/gpt-5.3-instant" }, + body: { prompt: "draw a kitten", n: 5, model: "cgpt-web/gpt-5.5" }, credentials: { apiKey: "test" }, log: null, }); diff --git a/tests/unit/live-model-catalog-reconciliation-8926.test.ts b/tests/unit/live-model-catalog-reconciliation-8926.test.ts index 2796071e43..5294c830fc 100644 --- a/tests/unit/live-model-catalog-reconciliation-8926.test.ts +++ b/tests/unit/live-model-catalog-reconciliation-8926.test.ts @@ -202,3 +202,45 @@ test("#8926: partial passthrough discovery remains non-authoritative", async () ["gpt-5.6-luna"] ); }); + +test("ChatGPT Web curated variants require their mapped upstream live slug", async () => { + const variants = new Map([ + ["gpt-5.6-sol-pro", "gpt-5-6-pro"], + ["gpt-5.6-sol-xhigh", "gpt-5-6-thinking"], + ["gpt-5.6-sol-high", "gpt-5-6-thinking"], + ["gpt-5.6-sol-medium", "gpt-5-6-thinking"], + ["gpt-5.6-sol-instant", "gpt-5-6"], + ["gpt-5.6-luna-free-thinking", "gpt-5-6"], + ["gpt-5.6-luna-free", "gpt-5-6"], + ["gpt-5.5-pro-extended", "gpt-5-5-pro"], + ["gpt-5.5-pro", "gpt-5-5-pro"], + ["gpt-5.5-xhigh", "gpt-5-5-thinking"], + ["gpt-5.5-high", "gpt-5-5-thinking"], + ["gpt-5.5-medium", "gpt-5-5-thinking"], + ["gpt-5.5-instant", "gpt-5-5"], + ]); + + await seedProviderCatalog( + "chatgpt-web", + "chatgpt-web-live-8926", + Array.from(new Set(variants.values())) + ); + + const catalog = await getActiveSyncedCatalog("chatgpt-web"); + assert.equal(catalog.authoritative, true); + + for (const modelId of variants.keys()) { + const resolved = await getModelInfo(`chatgpt-web/${modelId}`); + assert.equal(resolved.provider, "chatgpt-web", modelId); + assert.equal(resolved.model, modelId, modelId); + } + + await seedProviderCatalog("chatgpt-web", "chatgpt-web-live-8926", ["gpt-5-6"]); + + const available = await getModelInfo("chatgpt-web/gpt-5.6-sol-instant"); + assert.equal(available.provider, "chatgpt-web"); + + const unavailable = await getModelInfo("chatgpt-web/gpt-5.6-sol-pro"); + assert.equal(unavailable.provider, null); + assert.equal(unavailable.errorType, "model_not_found"); +}); diff --git a/tests/unit/model-listing-capability-5420.test.ts b/tests/unit/model-listing-capability-5420.test.ts index 0c7a6777e6..581a07e8cb 100644 --- a/tests/unit/model-listing-capability-5420.test.ts +++ b/tests/unit/model-listing-capability-5420.test.ts @@ -33,8 +33,10 @@ describe("providerLacksModelListing (#5420)", () => { it("keeps curated web providers visible while disabling remote model import", () => { assert.equal(providerLacksModelListing("kimi-web", ["llm"]), false); assert.equal(providerLacksModelListing("zai-web", ["llm"]), false); + assert.equal(providerLacksModelListing("chatgpt-web", ["llm"]), false); assert.equal(providerUsesCuratedModelsOnly("kimi-web"), true); assert.equal(providerUsesCuratedModelsOnly("zai-web"), true); + assert.equal(providerUsesCuratedModelsOnly("chatgpt-web"), true); assert.equal(providerUsesCuratedModelsOnly("qwen-cloud"), false); assert.equal(providerUsesCuratedModelsOnly("kimi-coding"), false); }); From 5e508147c4688c3459962884eb540ec8ae97181e Mon Sep 17 00:00:00 2001 From: backryun Date: Thu, 20 Aug 2026 23:32:08 +0900 Subject: [PATCH 052/135] perf(electron): defer hidden-start renderer creation (#10327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (6/6 focused lazy-window tests, plus the wider electron suite, typecheck:core clean, gates green). Thanks! --- electron/lib/windowLifecycle.js | 28 +++++++ electron/main.js | 68 +++++++-------- electron/package.json | 1 + tests/unit/electron-lazy-window.test.ts | 105 ++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 electron/lib/windowLifecycle.js create mode 100644 tests/unit/electron-lazy-window.test.ts diff --git a/electron/lib/windowLifecycle.js b/electron/lib/windowLifecycle.js new file mode 100644 index 0000000000..8a75a4a18d --- /dev/null +++ b/electron/lib/windowLifecycle.js @@ -0,0 +1,28 @@ +/** Pure helpers for deciding and driving the Electron dashboard window lifecycle. */ + +function shouldStartHidden({ argv = [], loginItemSettings = {} } = {}) { + return ( + argv.includes("--hidden") || + argv.includes("--minimized") || + loginItemSettings.wasOpenedAsHidden === true + ); +} + +function showOrCreateWindow({ appReady, getWindow, createWindow }) { + if (!appReady) return null; + + const currentWindow = getWindow(); + if (!currentWindow || currentWindow.isDestroyed()) { + return createWindow(); + } + + if (currentWindow.isMinimized()) currentWindow.restore(); + currentWindow.show(); + currentWindow.focus(); + return currentWindow; +} + +module.exports = { + shouldStartHidden, + showOrCreateWindow, +}; diff --git a/electron/main.js b/electron/main.js index 61f9e784d9..f2e3a4e3d8 100644 --- a/electron/main.js +++ b/electron/main.js @@ -40,6 +40,7 @@ const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper"); const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl"); const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences"); const { buildReadinessUrl, waitForServer } = require("./lib/serverReadiness"); +const { shouldStartHidden, showOrCreateWindow } = require("./lib/windowLifecycle"); // ── Single Instance Lock ─────────────────────────────────── const gotTheLock = app.requestSingleInstanceLock(); @@ -49,11 +50,7 @@ if (!gotTheLock) { } app.on("second-instance", () => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.show(); - mainWindow.focus(); - } + showMainWindow(); }); // ── Environment Detection ────────────────────────────────── @@ -71,6 +68,7 @@ let nextServer = null; let serverPort = 20128; let isServerStopped = false; let remoteServerPromptWindow = null; +let keepAliveWithoutWindows = false; // ── Remote Server Mode ────────────────────────────────────── // Lets the desktop shell attach to an already-running OmniRoute server (e.g. a @@ -366,6 +364,8 @@ function setupContentSecurityPolicy() { // ── Create Window ────────────────────────────────────────── function createWindow() { + if (mainWindow && !mainWindow.isDestroyed()) return mainWindow; + // Platform-conditional options (#9) const platformWindowOptions = process.platform === "darwin" @@ -397,16 +397,10 @@ function createWindow() { mainWindow.webContents.openDevTools({ mode: "detach" }); } - // Show window when ready (unless starting minimized/hidden in tray) + // Hidden startup skips createWindow() entirely; any created dashboard is explicit. mainWindow.once("ready-to-show", () => { - const startHidden = - process.argv.includes("--hidden") || - process.argv.includes("--minimized") || - app.getLoginItemSettings().wasOpenedAsHidden; - if (!startHidden) { + if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.show(); - } else { - console.log("[Electron] Launched hidden in background tray"); } }); @@ -437,6 +431,16 @@ function createWindow() { mainWindow.on("closed", () => { mainWindow = null; }); + + return mainWindow; +} + +function showMainWindow() { + return showOrCreateWindow({ + appReady: app.isReady(), + getWindow: () => mainWindow, + createWindow, + }); } // ── System Tray ──────────────────────────────────────────── @@ -465,12 +469,7 @@ function createTray() { const contextMenu = Menu.buildFromTemplate([ { label: "Open OmniRoute", - click: () => { - if (mainWindow) { - mainWindow.show(); - mainWindow.focus(); - } - }, + click: () => showMainWindow(), }, { label: "Open Dashboard", @@ -523,10 +522,7 @@ function createTray() { tray.setContextMenu(contextMenu); tray.on("double-click", () => { - if (mainWindow) { - mainWindow.show(); - mainWindow.focus(); - } + showMainWindow(); }); } @@ -1094,9 +1090,20 @@ app.whenReady().then(async () => { process.argv.includes("--headless") || process.argv.includes("--cli") || process.env.OMNIROUTE_HEADLESS === "true"; + const startHidden = + !isHeadless && + shouldStartHidden({ + argv: process.argv, + loginItemSettings: app.getLoginItemSettings(), + }); + keepAliveWithoutWindows = startHidden; // Fix #1: Start server and WAIT for readiness before showing window startNextServer(); + if (!isHeadless) { + createTray(); + } + let serverReady = true; if (!isDev) { // Probe the lightweight auth-exempt endpoint instead of aggregating full monitoring state. @@ -1105,9 +1112,10 @@ app.whenReady().then(async () => { if (isHeadless) { console.log("[Electron] Headless mode active — UI window and tray icon skipped"); + } else if (startHidden) { + console.log("[Electron] Launched hidden in background tray without a renderer"); } else { - createWindow(); - createTray(); + showMainWindow(); } setupIpcHandlers(); @@ -1115,7 +1123,7 @@ app.whenReady().then(async () => { // If readiness timed out (e.g. very long first-launch migrations), don't leave the // window stuck on a hanging connection — keep polling and reload once it responds (#2460). - if (!isDev && !serverReady && !isHeadless) { + if (!isDev && !serverReady && !isHeadless && !startHidden) { void waitForServer(getServerReadinessUrl(), 300000).then((ready) => { if (ready && mainWindow && !mainWindow.isDestroyed()) { mainWindow.loadURL(getServerUrl()); @@ -1133,11 +1141,7 @@ app.whenReady().then(async () => { // macOS: recreate window when dock icon clicked app.on("activate", () => { if (isHeadless) return; - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } else if (mainWindow) { - mainWindow.show(); - } + showMainWindow(); }); }); @@ -1147,7 +1151,7 @@ app.on("window-all-closed", () => { process.argv.includes("--headless") || process.argv.includes("--cli") || process.env.OMNIROUTE_HEADLESS === "true"; - if (process.platform !== "darwin" && !isHeadless) { + if (process.platform !== "darwin" && !isHeadless && !keepAliveWithoutWindows) { app.quit(); } }); diff --git a/electron/package.json b/electron/package.json index a3793de8fa..8a895b3566 100644 --- a/electron/package.json +++ b/electron/package.json @@ -64,6 +64,7 @@ "remoteServerPromptRenderer.js", "lib/resolveServerEntry.js", "lib/resolveNodeHelper.js", + "lib/windowLifecycle.js", "lib/resolveRemoteServerUrl.js", "lib/remoteServerPreferences.js", "lib/serverReadiness.js", diff --git a/tests/unit/electron-lazy-window.test.ts b/tests/unit/electron-lazy-window.test.ts new file mode 100644 index 0000000000..d3a4440c8b --- /dev/null +++ b/tests/unit/electron-lazy-window.test.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createRequire } from "node:module"; +import { describe, it } from "node:test"; + +const require = createRequire(import.meta.url); +const { shouldStartHidden, showOrCreateWindow } = require("../../electron/lib/windowLifecycle"); + +describe("Electron hidden-start window lifecycle", () => { + it("detects explicit hidden flags and OS login-item hidden launches", () => { + assert.equal(shouldStartHidden({ argv: ["electron", "--hidden"] }), true); + assert.equal(shouldStartHidden({ argv: ["electron", "--minimized"] }), true); + assert.equal( + shouldStartHidden({ argv: ["electron"], loginItemSettings: { wasOpenedAsHidden: true } }), + true + ); + assert.equal(shouldStartHidden({ argv: ["electron"], loginItemSettings: {} }), false); + }); + + it("creates the dashboard only when an explicit open action has no live window", () => { + const createdWindow = { id: "created" }; + let createCalls = 0; + const result = showOrCreateWindow({ + appReady: true, + getWindow: () => null, + createWindow: () => { + createCalls += 1; + return createdWindow; + }, + }); + + assert.equal(result, createdWindow); + assert.equal(createCalls, 1); + }); + + it("restores, shows, and focuses an existing dashboard without recreating it", () => { + const calls: string[] = []; + const existingWindow = { + isDestroyed: () => false, + isMinimized: () => true, + restore: () => calls.push("restore"), + show: () => calls.push("show"), + focus: () => calls.push("focus"), + }; + + const result = showOrCreateWindow({ + appReady: true, + getWindow: () => existingWindow, + createWindow: () => { + throw new Error("must not recreate a live dashboard"); + }, + }); + + assert.equal(result, existingWindow); + assert.deepEqual(calls, ["restore", "show", "focus"]); + }); + + it("does not create a BrowserWindow before Electron is ready", () => { + let createCalls = 0; + const result = showOrCreateWindow({ + appReady: false, + getWindow: () => null, + createWindow: () => { + createCalls += 1; + }, + }); + + assert.equal(result, null); + assert.equal(createCalls, 0); + }); + + it("routes tray, second-instance, and macOS activation opens through the lazy helper", () => { + const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8"); + + assert.match(mainSource, /app\.on\("second-instance", \(\) => \{\s*showMainWindow\(\);/); + assert.match(mainSource, /label: "Open OmniRoute",\s*click: \(\) => showMainWindow\(\)/); + assert.match(mainSource, /tray\.on\("double-click", \(\) => \{\s*showMainWindow\(\);/); + assert.match(mainSource, /app\.on\("activate", \(\) => \{[\s\S]*?showMainWindow\(\);/); + }); + + it("keeps hidden startup renderer-free until an explicit open action", () => { + const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8"); + const readyBlock = mainSource.slice(mainSource.indexOf("app.whenReady().then")); + + assert.match( + readyBlock, + /startNextServer\(\);\s*if \(!isHeadless\) \{\s*createTray\(\);\s*\}/, + "the server and tray must start before the hidden/visible renderer decision" + ); + assert.match( + readyBlock, + /if \(isHeadless\)[\s\S]*?else if \(startHidden\)[\s\S]*?else \{\s*showMainWindow\(\);/ + ); + assert.doesNotMatch( + readyBlock.match(/else if \(startHidden\)[\s\S]*?\} else \{/s)?.[0] ?? "", + /createWindow\(|showMainWindow\(/ + ); + assert.match( + readyBlock, + /\}\s*setupIpcHandlers\(\);\s*setupAutoUpdater\(\);/, + "IPC and updater setup must remain active when no renderer was created" + ); + }); +}); From 8122f6b71c02a594fdc0e1321bd4cef924a3fb70 Mon Sep 17 00:00:00 2001 From: backryun Date: Thu, 20 Aug 2026 23:33:45 +0900 Subject: [PATCH 053/135] perf(electron): optionally unload renderer on close (4/8) (#10328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (73/73 focused electron tests, typecheck:core clean, gates green) after resolving base-drift against #10327 (both landed today, real interleaved logic in createWindow/showMainWindow/window-all-closed — combined so the hidden-start lazy-open path from #10327 and the unload-on-close path from this PR both stay intact; verified by the existing test 'keeps the non-macOS app alive when unloading its last renderer'). Nice pair of electron perf PRs, thanks! --- electron/lib/remoteServerPreferences.js | 46 ++++++-- electron/lib/windowClosePolicy.js | 26 +++++ electron/main.js | 104 ++++++++++++++---- electron/package.json | 1 + tests/unit/electron-lazy-window.test.ts | 6 +- tests/unit/electron-remote-server.test.ts | 34 +++++- .../unit/electron-window-close-policy.test.ts | 87 +++++++++++++++ 7 files changed, 271 insertions(+), 33 deletions(-) create mode 100644 electron/lib/windowClosePolicy.js create mode 100644 tests/unit/electron-window-close-policy.test.ts diff --git a/electron/lib/remoteServerPreferences.js b/electron/lib/remoteServerPreferences.js index 21b683290f..71425e37b9 100644 --- a/electron/lib/remoteServerPreferences.js +++ b/electron/lib/remoteServerPreferences.js @@ -5,8 +5,8 @@ const path = require("path"); /** * remoteServerPreferences.js — pure read/write helpers for the small JSON - * preferences file that persists the operator-configured remote server URL - * across app restarts (see resolveRemoteServerUrl.js for how it's consumed). + * preferences file that persists desktop-shell choices needed before the + * server-owned settings database is available. * * Deliberately a plain flat JSON file rather than the app's SQLite database: * this preference must be readable before deciding whether to spawn (or even @@ -18,19 +18,20 @@ const path = require("path"); * @param {string} prefsPath - absolute path to electron-preferences.json * @param {(p: string) => boolean} [existsSync] * @param {(p: string, enc: string) => string} [readFileSync] - * @returns {{remoteServerUrl: string|null}} + * @returns {{remoteServerUrl: string|null, closeBehavior: "keep-loaded"|"unload"}} */ function readPreferences(prefsPath, existsSync = fs.existsSync, readFileSync = fs.readFileSync) { - if (!existsSync(prefsPath)) return { remoteServerUrl: null }; + if (!existsSync(prefsPath)) return { remoteServerUrl: null, closeBehavior: "keep-loaded" }; try { const parsed = JSON.parse(readFileSync(prefsPath, "utf8")); const remoteServerUrl = typeof parsed.remoteServerUrl === "string" && parsed.remoteServerUrl.trim() ? parsed.remoteServerUrl.trim() : null; - return { remoteServerUrl }; + const closeBehavior = parsed.closeBehavior === "unload" ? "unload" : "keep-loaded"; + return { remoteServerUrl, closeBehavior }; } catch { - return { remoteServerUrl: null }; + return { remoteServerUrl: null, closeBehavior: "keep-loaded" }; } } @@ -72,4 +73,35 @@ function writeRemoteServerUrl( } } -module.exports = { readPreferences, writeRemoteServerUrl }; +/** Persist whether closing the dashboard hides it or unloads its renderer. */ +function writeCloseBehavior( + prefsPath, + closeBehavior, + { + existsSync = fs.existsSync, + readFileSync = fs.readFileSync, + writeFileSync = fs.writeFileSync, + mkdirSync = fs.mkdirSync, + } = {} +) { + try { + const dir = path.dirname(prefsPath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + const current = readPreferences(prefsPath, existsSync, readFileSync); + const next = { + ...current, + closeBehavior: closeBehavior === "unload" ? "unload" : "keep-loaded", + }; + writeFileSync(prefsPath, JSON.stringify(next, null, 2) + "\n", "utf8"); + } catch (err) { + console.error( + `[remoteServerPreferences] Failed to write preferences to ${prefsPath}:`, + err instanceof Error ? err.message : String(err) + ); + } +} + +module.exports = { readPreferences, writeRemoteServerUrl, writeCloseBehavior }; diff --git a/electron/lib/windowClosePolicy.js b/electron/lib/windowClosePolicy.js new file mode 100644 index 0000000000..989e380d41 --- /dev/null +++ b/electron/lib/windowClosePolicy.js @@ -0,0 +1,26 @@ +"use strict"; + +const CLOSE_BEHAVIOR_KEEP_LOADED = "keep-loaded"; +const CLOSE_BEHAVIOR_UNLOAD = "unload"; + +function normalizeCloseBehavior(value) { + if (value === CLOSE_BEHAVIOR_KEEP_LOADED || value === CLOSE_BEHAVIOR_UNLOAD) return value; + return null; +} + +function resolveRendererUrl(currentUrl, serverUrl) { + try { + const current = new URL(currentUrl); + const server = new URL(serverUrl); + return current.origin === server.origin ? current.href : server.href; + } catch { + return serverUrl; + } +} + +module.exports = { + CLOSE_BEHAVIOR_KEEP_LOADED, + CLOSE_BEHAVIOR_UNLOAD, + normalizeCloseBehavior, + resolveRendererUrl, +}; diff --git a/electron/main.js b/electron/main.js index f2e3a4e3d8..19f232226b 100644 --- a/electron/main.js +++ b/electron/main.js @@ -38,9 +38,19 @@ const { killProcessTree } = require("./processTree"); const { resolveServerEntry } = require("./lib/resolveServerEntry"); const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper"); const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl"); -const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences"); +const { + readPreferences, + writeRemoteServerUrl, + writeCloseBehavior, +} = require("./lib/remoteServerPreferences"); const { buildReadinessUrl, waitForServer } = require("./lib/serverReadiness"); const { shouldStartHidden, showOrCreateWindow } = require("./lib/windowLifecycle"); +const { + CLOSE_BEHAVIOR_KEEP_LOADED, + CLOSE_BEHAVIOR_UNLOAD, + normalizeCloseBehavior, + resolveRendererUrl, +} = require("./lib/windowClosePolicy"); // ── Single Instance Lock ─────────────────────────────────── const gotTheLock = app.requestSingleInstanceLock(); @@ -50,6 +60,11 @@ if (!gotTheLock) { } app.on("second-instance", () => { + const isHeadless = + process.argv.includes("--headless") || + process.argv.includes("--cli") || + process.env.OMNIROUTE_HEADLESS === "true"; + if (isHeadless) return; showMainWindow(); }); @@ -69,6 +84,7 @@ let serverPort = 20128; let isServerStopped = false; let remoteServerPromptWindow = null; let keepAliveWithoutWindows = false; +let lastRendererUrl = null; // ── Remote Server Mode ────────────────────────────────────── // Lets the desktop shell attach to an already-running OmniRoute server (e.g. a @@ -79,6 +95,8 @@ const REMOTE_SERVER_PREFS_PATH = path.join( resolveDataDir(null, process.env), "electron-preferences.json" ); +const electronPreferences = readPreferences(REMOTE_SERVER_PREFS_PATH); +let closeBehavior = electronPreferences.closeBehavior; let remoteServerUrl = resolveRemoteServerUrl({ env: process.env, prefsPath: REMOTE_SERVER_PREFS_PATH, @@ -363,16 +381,18 @@ function setupContentSecurityPolicy() { } // ── Create Window ────────────────────────────────────────── -function createWindow() { +function createWindow({ showWhenReady = true } = {}) { if (mainWindow && !mainWindow.isDestroyed()) return mainWindow; + const rendererStartedAt = Date.now(); + // Platform-conditional options (#9) const platformWindowOptions = process.platform === "darwin" ? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 16 } } : { titleBarStyle: "default" }; - mainWindow = new BrowserWindow({ + const window = new BrowserWindow({ width: 1400, height: 900, minWidth: 1024, @@ -390,22 +410,28 @@ function createWindow() { backgroundColor: "#0a0a0a", ...platformWindowOptions, }); + mainWindow = window; // Load the Next.js app - mainWindow.loadURL(getServerUrl()); + window.loadURL(resolveRendererUrl(lastRendererUrl, getServerUrl())); if (isDev) { - mainWindow.webContents.openDevTools({ mode: "detach" }); + window.webContents.openDevTools({ mode: "detach" }); } - // Hidden startup skips createWindow() entirely; any created dashboard is explicit. - mainWindow.once("ready-to-show", () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.show(); + // Hidden startup (createWindow({ showWhenReady: false })) skips the initial + // show(); the window stays created (so tray/dock interactions work) but the + // renderer only becomes visible on the next explicit showMainWindow() call. + window.once("ready-to-show", () => { + console.log(`[Electron] Renderer ready in ${Date.now() - rendererStartedAt}ms`); + if (showWhenReady) { + window.show(); + } else { + console.log("[Electron] Launched hidden in background tray"); } }); // Handle external links — validate URL protocol to prevent RCE - mainWindow.webContents.setWindowOpenHandler(({ url }) => { + window.webContents.setWindowOpenHandler(({ url }) => { try { const parsedUrl = new URL(url); if (["http:", "https:"].includes(parsedUrl.protocol)) { @@ -419,20 +445,28 @@ function createWindow() { return { action: "deny" }; }); - // Handle window close — minimize to tray - mainWindow.on("close", (event) => { + // Keep the server alive while either hiding the renderer for a fast reopen or + // unloading it to reclaim memory, according to the persisted tray preference. + window.on("close", (event) => { if (!app.isQuitting) { event.preventDefault(); - mainWindow.hide(); + lastRendererUrl = resolveRendererUrl(window.webContents.getURL(), getServerUrl()); + if (closeBehavior === CLOSE_BEHAVIOR_UNLOAD) { + console.log("[Electron] Dashboard renderer unloaded; server remains running"); + window.destroy(); + } else { + console.log("[Electron] Dashboard hidden; renderer kept loaded"); + window.hide(); + } } return false; }); - mainWindow.on("closed", () => { - mainWindow = null; + window.on("closed", () => { + if (mainWindow === window) mainWindow = null; }); - return mainWindow; + return window; } function showMainWindow() { @@ -443,6 +477,14 @@ function showMainWindow() { }); } +function setCloseBehavior(nextBehavior) { + const normalized = normalizeCloseBehavior(nextBehavior); + if (!normalized || normalized === closeBehavior) return; + closeBehavior = normalized; + writeCloseBehavior(REMOTE_SERVER_PREFS_PATH, closeBehavior); + createTray(); +} + // ── System Tray ──────────────────────────────────────────── function createTray() { // Fix #4: Destroy old tray before recreating @@ -503,6 +545,23 @@ function createTray() { }, ], }, + { + label: "When Dashboard Closes", + submenu: [ + { + label: "Keep Loaded (Faster Reopen)", + type: "radio", + checked: closeBehavior === CLOSE_BEHAVIOR_KEEP_LOADED, + click: () => setCloseBehavior(CLOSE_BEHAVIOR_KEEP_LOADED), + }, + { + label: "Unload Renderer (Lower Memory)", + type: "radio", + checked: closeBehavior === CLOSE_BEHAVIOR_UNLOAD, + click: () => setCloseBehavior(CLOSE_BEHAVIOR_UNLOAD), + }, + ], + }, { type: "separator" }, { label: "Check for Updates", @@ -521,9 +580,7 @@ function createTray() { tray.setToolTip("OmniRoute"); tray.setContextMenu(contextMenu); - tray.on("double-click", () => { - showMainWindow(); - }); + tray.on("double-click", () => showMainWindow()); } // ── Change Port (#3: now restarts server) ────────────────── @@ -545,6 +602,7 @@ async function changePort(newPort) { await waitForServer(getServerReadinessUrl()); // Reload window and update tray + lastRendererUrl = getServerUrl(); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.loadURL(getServerUrl()); } @@ -609,6 +667,7 @@ async function setRemoteServerUrl(nextUrl) { remoteServerUrl = normalized; writeRemoteServerUrl(REMOTE_SERVER_PREFS_PATH, remoteServerUrl); + lastRendererUrl = getServerUrl(); startNextServer(); try { @@ -1151,7 +1210,12 @@ app.on("window-all-closed", () => { process.argv.includes("--headless") || process.argv.includes("--cli") || process.env.OMNIROUTE_HEADLESS === "true"; - if (process.platform !== "darwin" && !isHeadless && !keepAliveWithoutWindows) { + if ( + process.platform !== "darwin" && + !isHeadless && + !keepAliveWithoutWindows && + closeBehavior !== CLOSE_BEHAVIOR_UNLOAD + ) { app.quit(); } }); diff --git a/electron/package.json b/electron/package.json index 8a895b3566..57789a4318 100644 --- a/electron/package.json +++ b/electron/package.json @@ -68,6 +68,7 @@ "lib/resolveRemoteServerUrl.js", "lib/remoteServerPreferences.js", "lib/serverReadiness.js", + "lib/windowClosePolicy.js", "assets/remoteServerPrompt.html", "package.json", "node_modules/**/*" diff --git a/tests/unit/electron-lazy-window.test.ts b/tests/unit/electron-lazy-window.test.ts index d3a4440c8b..f9d3f97677 100644 --- a/tests/unit/electron-lazy-window.test.ts +++ b/tests/unit/electron-lazy-window.test.ts @@ -73,9 +73,11 @@ describe("Electron hidden-start window lifecycle", () => { it("routes tray, second-instance, and macOS activation opens through the lazy helper", () => { const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8"); - assert.match(mainSource, /app\.on\("second-instance", \(\) => \{\s*showMainWindow\(\);/); + // #10328 added a headless guard ahead of the lazy-open call; second-instance + // must still route through showMainWindow() once past that guard. + assert.match(mainSource, /app\.on\("second-instance", \(\) => \{[\s\S]*?showMainWindow\(\);/); assert.match(mainSource, /label: "Open OmniRoute",\s*click: \(\) => showMainWindow\(\)/); - assert.match(mainSource, /tray\.on\("double-click", \(\) => \{\s*showMainWindow\(\);/); + assert.match(mainSource, /tray\.on\("double-click", \(\) => showMainWindow\(\)\);/); assert.match(mainSource, /app\.on\("activate", \(\) => \{[\s\S]*?showMainWindow\(\);/); }); diff --git a/tests/unit/electron-remote-server.test.ts b/tests/unit/electron-remote-server.test.ts index 05903db782..9595a0d6fe 100644 --- a/tests/unit/electron-remote-server.test.ts +++ b/tests/unit/electron-remote-server.test.ts @@ -25,6 +25,7 @@ const { const { readPreferences, writeRemoteServerUrl, + writeCloseBehavior, } = require("../../electron/lib/remoteServerPreferences"); function withTempDir(fn: (dir: string) => void) { @@ -126,7 +127,10 @@ describe("remoteServerPreferences read/write", () => { withTempDir((dir) => { const prefsPath = join(dir, "electron-preferences.json"); writeRemoteServerUrl(prefsPath, "http://localhost:20128"); - assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: "http://localhost:20128" }); + assert.deepEqual(readPreferences(prefsPath), { + remoteServerUrl: "http://localhost:20128", + closeBehavior: "keep-loaded", + }); }); }); @@ -135,7 +139,10 @@ describe("remoteServerPreferences read/write", () => { const prefsPath = join(dir, "electron-preferences.json"); writeRemoteServerUrl(prefsPath, "http://localhost:20128"); writeRemoteServerUrl(prefsPath, null); - assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: null }); + assert.deepEqual(readPreferences(prefsPath), { + remoteServerUrl: null, + closeBehavior: "keep-loaded", + }); }); }); @@ -144,14 +151,32 @@ describe("remoteServerPreferences read/write", () => { const prefsPath = join(dir, "nested", "deep", "electron-preferences.json"); assert.doesNotThrow(() => writeRemoteServerUrl(prefsPath, "http://localhost:20128")); assert.equal(existsSync(prefsPath), true); - assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: "http://localhost:20128" }); + assert.deepEqual(readPreferences(prefsPath), { + remoteServerUrl: "http://localhost:20128", + closeBehavior: "keep-loaded", + }); }); }); it("reading a nonexistent prefs file returns remoteServerUrl: null", () => { withTempDir((dir) => { const prefsPath = join(dir, "electron-preferences.json"); - assert.deepEqual(readPreferences(prefsPath), { remoteServerUrl: null }); + assert.deepEqual(readPreferences(prefsPath), { + remoteServerUrl: null, + closeBehavior: "keep-loaded", + }); + }); + }); + + it("persists close behavior without discarding the remote server URL", () => { + withTempDir((dir) => { + const prefsPath = join(dir, "electron-preferences.json"); + writeRemoteServerUrl(prefsPath, "https://omniroute.example.com"); + writeCloseBehavior(prefsPath, "unload"); + assert.deepEqual(readPreferences(prefsPath), { + remoteServerUrl: "https://omniroute.example.com", + closeBehavior: "unload", + }); }); }); }); @@ -243,6 +268,7 @@ describe("Electron packaging manifest includes Remote Server Mode files", () => for (const expected of [ "lib/resolveRemoteServerUrl.js", "lib/remoteServerPreferences.js", + "lib/windowClosePolicy.js", "remoteServerPromptPreload.js", "remoteServerPromptRenderer.js", "assets/remoteServerPrompt.html", diff --git a/tests/unit/electron-window-close-policy.test.ts b/tests/unit/electron-window-close-policy.test.ts new file mode 100644 index 0000000000..1febea3b6f --- /dev/null +++ b/tests/unit/electron-window-close-policy.test.ts @@ -0,0 +1,87 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { + CLOSE_BEHAVIOR_KEEP_LOADED, + CLOSE_BEHAVIOR_UNLOAD, + normalizeCloseBehavior, + resolveRendererUrl, +} = require("../../electron/lib/windowClosePolicy"); + +describe("Electron window close policy", () => { + it("accepts only the two deliberate close behaviors", () => { + assert.equal(normalizeCloseBehavior("keep-loaded"), CLOSE_BEHAVIOR_KEEP_LOADED); + assert.equal(normalizeCloseBehavior("unload"), CLOSE_BEHAVIOR_UNLOAD); + assert.equal(normalizeCloseBehavior("destroy"), null); + assert.equal(normalizeCloseBehavior(undefined), null); + }); + + it("preserves same-origin dashboard navigation when recreating the renderer", () => { + assert.equal( + resolveRendererUrl( + "http://localhost:20128/dashboard/settings?tab=providers", + "http://localhost:20128" + ), + "http://localhost:20128/dashboard/settings?tab=providers" + ); + }); + + it("falls back to the active server for invalid or cross-origin URLs", () => { + assert.equal( + resolveRendererUrl("https://example.com/dashboard", "http://localhost:20128"), + "http://localhost:20128/" + ); + assert.equal( + resolveRendererUrl("not a url", "http://localhost:20128"), + "http://localhost:20128" + ); + }); +}); + +describe("Electron main-process close policy wiring", () => { + const mainSrc = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8"); + + it("defaults to keeping the renderer loaded and exposes both policies in the tray", () => { + assert.match(mainSrc, /electronPreferences\.closeBehavior/); + assert.match(mainSrc, /Keep Loaded \(Faster Reopen\)/); + assert.match(mainSrc, /Unload Renderer \(Lower Memory\)/); + assert.match(mainSrc, /writeCloseBehavior\(REMOTE_SERVER_PREFS_PATH, closeBehavior\)/); + }); + + it("destroys only the renderer in unload mode and otherwise hides the window", () => { + const closeHandler = mainSrc.slice( + mainSrc.indexOf('window.on("close"'), + mainSrc.indexOf('window.on("closed"') + ); + assert.ok(closeHandler.includes("closeBehavior === CLOSE_BEHAVIOR_UNLOAD")); + assert.ok(closeHandler.includes("window.destroy()")); + assert.ok(closeHandler.includes("window.hide()")); + assert.ok(!closeHandler.includes("stopNextServer")); + }); + + it("recreates the renderer from every explicit reopen path", () => { + const secondInstanceHandler = mainSrc.slice( + mainSrc.indexOf('app.on("second-instance"'), + mainSrc.indexOf("// ── Environment Detection") + ); + assert.ok(secondInstanceHandler.includes("showMainWindow()")); + assert.ok(secondInstanceHandler.includes("if (isHeadless) return")); + assert.match(mainSrc, /label: "Open OmniRoute",\s*click: \(\) => showMainWindow\(\)/); + assert.match(mainSrc, /tray\.on\("double-click", \(\) => showMainWindow\(\)\)/); + assert.match( + mainSrc, + /app\.on\("activate", \(\) => \{\s*if \(isHeadless\) return;\s*showMainWindow\(\);/ + ); + }); + + it("keeps the non-macOS app alive when unloading its last renderer", () => { + assert.match( + mainSrc, + /process\.platform !== "darwin"[\s\S]*closeBehavior !== CLOSE_BEHAVIOR_UNLOAD[\s\S]*app\.quit\(\)/ + ); + }); +}); From 80a59c0ae56e8572795f39faef93ea7165fa2eed Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:34 +0200 Subject: [PATCH 054/135] fix(images): route bare dall-e-3 to OpenAI (#10847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- changelog.d/fixes/10832-unprefixed-dalle3.md | 1 + open-sse/config/imageRegistry.ts | 1 + tests/unit/unprefixed-dalle3-10832.test.ts | 43 ++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 changelog.d/fixes/10832-unprefixed-dalle3.md create mode 100644 tests/unit/unprefixed-dalle3-10832.test.ts diff --git a/changelog.d/fixes/10832-unprefixed-dalle3.md b/changelog.d/fixes/10832-unprefixed-dalle3.md new file mode 100644 index 0000000000..2dfd970b13 --- /dev/null +++ b/changelog.d/fixes/10832-unprefixed-dalle3.md @@ -0,0 +1 @@ +- **fix(images):** register OpenAI `dall-e-3` in the image registry so unprefixed `dall-e-3` (and `openai/dall-e-3`) route to OpenAI Images instead of Microsoft Designer Web, and so the chat catalog no longer lists `openai/dall-e-3` as a 128k chat model ([#10832](https://github.com/diegosouzapw/OmniRoute/issues/10832)) diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index d50865599d..17ce7de90e 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -210,6 +210,7 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "openai", // native OpenAI format models: [ + { id: "dall-e-3", name: "DALL·E 3" }, { id: "gpt-image-2", name: "GPT Image 2" }, { id: "gpt-image-1.5", name: "GPT Image 1.5" }, { id: "gpt-image-1-mini", name: "GPT Image 1 Mini" }, diff --git a/tests/unit/unprefixed-dalle3-10832.test.ts b/tests/unit/unprefixed-dalle3-10832.test.ts new file mode 100644 index 0000000000..ae6200cbf5 --- /dev/null +++ b/tests/unit/unprefixed-dalle3-10832.test.ts @@ -0,0 +1,43 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + IMAGE_PROVIDERS, + parseImageModel, + getAllImageModels, + isRegisteredImageModel, +} from "../../open-sse/config/imageRegistry.ts"; + +test("#10832 unprefixed dall-e-3 routes to OpenAI, not Microsoft Designer Web", () => { + assert.deepEqual(parseImageModel("dall-e-3"), { + provider: "openai", + model: "dall-e-3", + }); + assert.deepEqual(parseImageModel("openai/dall-e-3"), { + provider: "openai", + model: "dall-e-3", + }); + assert.deepEqual(parseImageModel("microsoft-designer-web/dall-e-3"), { + provider: "microsoft-designer-web", + model: "dall-e-3", + }); + assert.deepEqual(parseImageModel("msdesigner/dall-e-3"), { + provider: "microsoft-designer-web", + model: "dall-e-3", + }); + + assert.equal(isRegisteredImageModel("openai", "dall-e-3"), true); + assert.equal(isRegisteredImageModel("microsoft-designer-web", "dall-e-3"), true); + + const openai = IMAGE_PROVIDERS.openai; + assert.ok(openai.models.some((model) => model.id === "dall-e-3")); + + const catalog = getAllImageModels(); + assert.ok(catalog.some((model) => model.id === "openai/dall-e-3" && model.provider === "openai")); + assert.ok( + catalog.some( + (model) => + model.id === "microsoft-designer-web/dall-e-3" && + model.provider === "microsoft-designer-web" + ) + ); +}); From 84d7e33c2650b5c56f0330e39bb63739dd300794 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:38 +0200 Subject: [PATCH 055/135] feat(resilience): warn on slow /healthz event-loop lag (#10827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../features/10303-healthz-event-loop-lag.md | 1 + docs/ops/MONITORING_GUIDE.md | 2 +- src/app/healthz/route.ts | 2 + src/lib/healthzLag.ts | 40 +++++++++++++++++++ tests/unit/10303-healthz-lag.test.ts | 35 ++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 changelog.d/features/10303-healthz-event-loop-lag.md create mode 100644 src/lib/healthzLag.ts create mode 100644 tests/unit/10303-healthz-lag.test.ts diff --git a/changelog.d/features/10303-healthz-event-loop-lag.md b/changelog.d/features/10303-healthz-event-loop-lag.md new file mode 100644 index 0000000000..991c123021 --- /dev/null +++ b/changelog.d/features/10303-healthz-event-loop-lag.md @@ -0,0 +1 @@ +- **feat(resilience):** warn when `/healthz` is served under event-loop lag ≥200ms so a slow 200 is visible as sick, not healthy ([#10303](https://github.com/diegosouzapw/OmniRoute/issues/10303)) diff --git a/docs/ops/MONITORING_GUIDE.md b/docs/ops/MONITORING_GUIDE.md index 82a66eeb49..a25add9242 100644 --- a/docs/ops/MONITORING_GUIDE.md +++ b/docs/ops/MONITORING_GUIDE.md @@ -162,7 +162,7 @@ OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHE | Probe | Recommended target | Notes | | --- | --- | --- | | **Startup** | HTTP `GET /healthz` with a long `failureThreshold` (or large `startPeriod`) | Cold start + SQLite migration can exceed a few seconds | -| **Readiness** | HTTP `GET /healthz` | Remove endpoints while starting/stopping; still flaps if the loop is CPU-blocked | +| **Readiness** | HTTP `GET /healthz` | Remove endpoints while starting/stopping; still flaps if the loop is CPU-blocked. A **200 in multiple seconds is not healthy** (#10303) — it means the event loop was starved before the 3-byte handler ran | | **Liveness** | **TCP** on the main service port (`PORT`, default `20128`), **or** HTTP `/healthz` with soft thresholds | Do **not** kill the pod on short event-loop stalls; busy ≠ dead | | **Deep health** | `GET /api/monitoring/health` from an external checker | Not for kubelet `livenessProbe` / tight `readinessProbe` | diff --git a/src/app/healthz/route.ts b/src/app/healthz/route.ts index 4ee01c32d2..5ac54700fd 100644 --- a/src/app/healthz/route.ts +++ b/src/app/healthz/route.ts @@ -1,4 +1,5 @@ import { getServerLifecyclePhase } from "@/lib/serverLifecycle"; +import { observeHealthzEventLoopLag } from "@/lib/healthzLag"; export const dynamic = "force-dynamic"; @@ -23,6 +24,7 @@ function createHealthResponse(method: "GET" | "HEAD"): Response { } export function GET(): Response { + observeHealthzEventLoopLag(); return createHealthResponse("GET"); } diff --git a/src/lib/healthzLag.ts b/src/lib/healthzLag.ts new file mode 100644 index 0000000000..fb4f17567e --- /dev/null +++ b/src/lib/healthzLag.ts @@ -0,0 +1,40 @@ +import { monitorEventLoopDelay } from "node:perf_hooks"; + +/** `/healthz` returning 200 after this much event-loop lag is already sick (#10303). */ +export const HEALTHZ_SLOW_LAG_MS = 200; +const WARN_EVERY_MS = 10_000; + +let lastWarnAt = 0; +let histogram: ReturnType | null = null; + +export function resetHealthzLagWarnStateForTests(): void { + lastWarnAt = 0; +} + +export function shouldWarnHealthzLag(lagMs: number, now = Date.now()): boolean { + if (!Number.isFinite(lagMs) || lagMs < HEALTHZ_SLOW_LAG_MS) return false; + if (now - lastWarnAt < WARN_EVERY_MS) return false; + lastWarnAt = now; + return true; +} + +export function formatHealthzLagWarning(lagMs: number): string { + return `GET /healthz event-loop lag ${Math.round(lagMs)}ms (HTTP 200 is not healthy; busy != ready)`; +} + +export function getEventLoopLagMs(): number { + if (!histogram) { + histogram = monitorEventLoopDelay({ resolution: 20 }); + histogram.enable(); + } + return histogram.mean / 1e6; +} + +export function observeHealthzEventLoopLag( + log: (msg: string) => void = console.warn, + lagMs = getEventLoopLagMs() +): boolean { + if (!shouldWarnHealthzLag(lagMs)) return false; + log(`[HEALTHZ] ${formatHealthzLagWarning(lagMs)}`); + return true; +} diff --git a/tests/unit/10303-healthz-lag.test.ts b/tests/unit/10303-healthz-lag.test.ts new file mode 100644 index 0000000000..afaab6e77f --- /dev/null +++ b/tests/unit/10303-healthz-lag.test.ts @@ -0,0 +1,35 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + HEALTHZ_SLOW_LAG_MS, + formatHealthzLagWarning, + observeHealthzEventLoopLag, + resetHealthzLagWarnStateForTests, + shouldWarnHealthzLag, +} from "../../src/lib/healthzLag.ts"; + +test("shouldWarnHealthzLag ignores sub-threshold lag", () => { + resetHealthzLagWarnStateForTests(); + assert.equal(shouldWarnHealthzLag(0), false); + assert.equal(shouldWarnHealthzLag(HEALTHZ_SLOW_LAG_MS - 1), false); +}); + +test("shouldWarnHealthzLag fires once then debounce", () => { + resetHealthzLagWarnStateForTests(); + const t0 = 1_700_000_000_000; + assert.equal(shouldWarnHealthzLag(3748, t0), true); + assert.equal(shouldWarnHealthzLag(3748, t0 + 1000), false); + assert.equal(shouldWarnHealthzLag(3748, t0 + 10_000), true); +}); + +test("observeHealthzEventLoopLag logs when injected lag is high", () => { + resetHealthzLagWarnStateForTests(); + const messages: string[] = []; + assert.equal(observeHealthzEventLoopLag((m) => messages.push(m), 12), false); + assert.equal(messages.length, 0); + assert.equal(observeHealthzEventLoopLag((m) => messages.push(m), 3748), true); + assert.equal(messages[0], `[HEALTHZ] ${formatHealthzLagWarning(3748)}`); + assert.match(messages[0], /3748ms/); + assert.match(messages[0], /not healthy/); +}); From 7ac6bbba377883a860320f4586081b1e07f43c98 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:43 +0200 Subject: [PATCH 056/135] docs(backend): document memory/skills/token-refresh event-loop cost (#10825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../maintenance/10349-optional-work-event-loop.md | 1 + docs/ops/MONITORING_GUIDE.md | 5 +++++ docs/reference/ENVIRONMENT.md | 13 +++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 changelog.d/maintenance/10349-optional-work-event-loop.md diff --git a/changelog.d/maintenance/10349-optional-work-event-loop.md b/changelog.d/maintenance/10349-optional-work-event-loop.md new file mode 100644 index 0000000000..0cd695e4a7 --- /dev/null +++ b/changelog.d/maintenance/10349-optional-work-event-loop.md @@ -0,0 +1 @@ +- **docs(backend):** document that memory extraction, skills injection, and token refresh share the request event loop, plus dashboard kill switches ([#10349](https://github.com/diegosouzapw/OmniRoute/issues/10349)) diff --git a/docs/ops/MONITORING_GUIDE.md b/docs/ops/MONITORING_GUIDE.md index a25add9242..da1dc7d39b 100644 --- a/docs/ops/MONITORING_GUIDE.md +++ b/docs/ops/MONITORING_GUIDE.md @@ -197,6 +197,11 @@ livenessProbe: Related: [#10052](https://github.com/diegosouzapw/OmniRoute/issues/10052) (probes while the event loop is busy), [#9685](https://github.com/diegosouzapw/OmniRoute/issues/9685) / [#10055](https://github.com/diegosouzapw/OmniRoute/pull/10055) (catalog pricing hog), [#10117](https://github.com/diegosouzapw/OmniRoute/issues/10117) (compression token-count hog). + +### Optional request-path work (memory, skills, token refresh) + +Memory extraction, skills injection, and OAuth token refresh share the **main Node event loop** with `/healthz`. They are dashboard-toggle features (`memoryEnabled`, `skillsEnabled`), not a worker pool. See [Environment — event-loop cost](../reference/ENVIRONMENT.md#event-loop-cost-of-memory-skills-and-token-refresh-10349). + ### Provider Health > **No REST endpoint.** Provider health data is available via the MCP tool `observability_snapshot` or the dashboard `/dashboard/providers` page. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 44af6e49b4..11f53408d0 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -862,6 +862,19 @@ The logging system writes to both stdout and rotated log files. All configuratio ### Memory Engine (plan 21) +### Event-loop cost of memory, skills, and token refresh (#10349) + +OmniRoute is a **single Node process**. Memory extraction/retrieval, skills injection, and provider token refresh run on that **same event loop** as `GET /healthz` and the dashboard. They are not a worker thread. + +| Work | Code | Default | Operator control | +| --- | --- | --- | --- | +| Memory extraction / retrieval | `src/lib/memory/` | Dashboard **memoryEnabled** (default on) | Turn off **Settings → Memory**. There is no separate env kill switch beyond disabling the feature in settings. | +| Skills injection | `src/lib/skills/injection.ts` | Dashboard **skillsEnabled** (default on) | Turn off **Settings → Memory/Skills** (`skillsEnabled`). Sandbox knobs below only bound execution after injection is already on. | +| Token refresh | `src/sse/services/tokenRefresh.ts` | On for connected OAuth/web providers | Disconnect the provider or let tokens stay valid; there is no `TOKEN_REFRESH=0` env today. | + +If `/healthz` is slow on a quiet box, disable memory + skills first, then check catalog/compression load (#10303, #9685). These features yield at `await` points but still compete for the one thread. + + Embedding layer, vector store and reranking knobs for the persistent memory subsystem (`src/lib/memory/`). | Variable | Default | Description | From 6767f270110bff5c526860976329559a13d919ec Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:47 +0200 Subject: [PATCH 057/135] docs(db): document throttled pre-write SQLite backups (#10824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- changelog.d/maintenance/10351-pre-write-backup-throttle.md | 1 + docs/reference/ENVIRONMENT.md | 4 ++-- src/lib/db/backup.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/maintenance/10351-pre-write-backup-throttle.md diff --git a/changelog.d/maintenance/10351-pre-write-backup-throttle.md b/changelog.d/maintenance/10351-pre-write-backup-throttle.md new file mode 100644 index 0000000000..f6141d30ea --- /dev/null +++ b/changelog.d/maintenance/10351-pre-write-backup-throttle.md @@ -0,0 +1 @@ +- **docs(backend):** document that pre-write SQLite backups (including models.dev pricing) are throttled to once per 60 minutes and can be disabled with `DISABLE_SQLITE_AUTO_BACKUP` ([#10351](https://github.com/diegosouzapw/OmniRoute/issues/10351)) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 11f53408d0..b35d7c97d9 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -91,7 +91,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari | `OMNIROUTE_DATA_DIR` | _(unset)_ | `open-sse/executors/promptql/threadSticky.ts` | **Fallback alias** for `DATA_DIR`, checked only when `DATA_DIR` is unset. Used to locate the PromptQL executor's on-disk thread-sticky session cache (`/promptql-thread-sessions.json`); if neither var is set, the cache stays in-memory only (not persisted across restarts). | | `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. | | `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. | -| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. | +| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips automatic + pre-write SQLite file backups (startup, models.dev pricing save/clear, settings writes). Manual and pre-restore backups still run. Non-manual backups are also **throttled to at most once per 60 minutes** so hourly models.dev sync does not copy the whole DB on every pricing write. Dashboard **Settings → Storage** can disable auto-backup independently. | | `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. | | `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. | | `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. | @@ -983,7 +983,7 @@ desktop install. | Variable | Default | Source File | Description | | ----------------------------------- | ------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MODELS_DEV_SYNC_ENABLED` | _(unset)_ | `src/lib/modelsDevSync.ts` | Hard override for models.dev pricing sync. Unset = honor Settings > AI (`modelsDevSyncEnabled`). `0`/`false`/`off`/`no` **wins over the DB** and skips both periodic sync and `getModelsDevPricing()` SQL/JSON scans (recovery when the dashboard is wedged on the same event loop). `1`/`true`/`on`/`yes` forces sync on. | +| `MODELS_DEV_SYNC_ENABLED` | _(unset)_ | `src/lib/modelsDevSync.ts` | Hard override for models.dev pricing sync. Unset = honor Settings > AI (`modelsDevSyncEnabled`). `0`/`false`/`off`/`no` **wins over the DB** and skips both periodic sync and `getModelsDevPricing()` SQL/JSON scans (recovery when the dashboard is wedged on the same event loop). `1`/`true`/`on`/`yes` forces sync on. Pricing save/clear still call `backupDbFile("pre-write")`, which is no-op under the 60-minute throttle or `DISABLE_SQLITE_AUTO_BACKUP`. | | `MODELS_DEV_SYNC_INTERVAL` | `86400` (24h) | `src/lib/modelsDevSync.ts` | Development-time model catalog sync interval in seconds. | | `CONTEXT_WINDOW_RECONCILE_INTERVAL` | `86400` (24h) | `src/lib/contextWindowResolver.ts` | Interval (seconds) for the self-correcting context-window reconciler (5004): pins provider-declared windows from `/models` discovery as `auto:discovery` overrides when they diverge from the catalog. Set to `0` to disable. Reuses already-synced data (no new fetch); never overwrites `manual` overrides. | diff --git a/src/lib/db/backup.ts b/src/lib/db/backup.ts index 3317ddb9fe..effbaf09c0 100644 --- a/src/lib/db/backup.ts +++ b/src/lib/db/backup.ts @@ -28,7 +28,7 @@ type CountRow = { cnt?: number }; // ──────────────── Backup Config ──────────────── let _lastBackupAt = 0; -const BACKUP_THROTTLE_MS = 60 * 60 * 1000; // 60 minutes +const BACKUP_THROTTLE_MS = 60 * 60 * 1000; // 60 minutes — high-churn pre-write (models.dev pricing) must not copy the whole SQLite file every call (#10351) const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]); // #3834: the "Keep latest backups" UI value is persisted here so it survives a page From 8b52596d7c8c7cf91480bb53d0cfa398f3daf07a Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:50 +0200 Subject: [PATCH 058/135] docs(auth): distinguish access tokens, API keys, and management credentials (#10823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../maintenance/7786-management-auth-guide.md | 1 + docs/guides/MANAGEMENT-AUTH.md | 166 +++++++++++++++--- docs/openapi.yaml | 10 +- docs/providers/ZED-DOCKER.md | 2 +- docs/reference/API_REFERENCE.md | 9 + public/openapi.yaml | 10 +- .../settings/components/AccessTokensTab.tsx | 2 +- 7 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 changelog.d/maintenance/7786-management-auth-guide.md diff --git a/changelog.d/maintenance/7786-management-auth-guide.md b/changelog.d/maintenance/7786-management-auth-guide.md new file mode 100644 index 0000000000..54f84a79b9 --- /dev/null +++ b/changelog.d/maintenance/7786-management-auth-guide.md @@ -0,0 +1 @@ +- **docs(auth):** distinguish dashboard sessions, `oma_live_…` Access Tokens, manage-scoped API keys, and inference keys ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786)) diff --git a/docs/guides/MANAGEMENT-AUTH.md b/docs/guides/MANAGEMENT-AUTH.md index 25e0d7ae59..31391e4b7f 100644 --- a/docs/guides/MANAGEMENT-AUTH.md +++ b/docs/guides/MANAGEMENT-AUTH.md @@ -1,47 +1,159 @@ --- title: "Management Authentication" version: 3.8.50 -lastUpdated: 2026-08-05 +lastUpdated: 2026-08-20 --- # Management Authentication -OmniRoute uses four distinct credential families for management access. This guide -distinguishes them by purpose, scope, and locality. +OmniRoute has **four credential families** that can authorize management routes. +They are not interchangeable. Inference API keys (`sk-…`) do **not** manage the +server unless they were explicitly granted `manage` or `admin` scope. -| Credential | Scope | Locality | Use Case | -|-------------------------|--------------------|---------------|-----------------------------------| -| Dashboard JWT session | Full management | Localhost | Web dashboard login | -| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands | -| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access | -| Manage-scope API key | `manage` scope | External | Management API calls | +Canonical implementation: `src/lib/api/requireManagementAuth.ts`. -## Dashboard JWT Session +| Credential | Typical form | Created where | Intended use | Management capability | +|---|---|---|---|---| +| Dashboard session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules | +| Local CLI machine token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only | +| Scoped Access Token | `oma_live_…` | **Settings → Access Tokens** or `omniroute connect` | Remote CLI and management API | Must satisfy the route's required `read`, `write`, or `admin` scope | +| Inference API key | `sk-…` (and other API-key prefixes) | **API Manager / API Keys** | `/v1/*` inference | **None** unless the key metadata includes `manage` or `admin` | -Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie. -Valid for the session duration. Cannot be used from external hosts. +`oma_` credentials are management/CLI credentials. They are **not** inference API keys. -## CLI Machine-ID Token +If login/API-key auth is disabled for the server, some management routes may +accept unauthenticated calls. Local-only and always-protected routes still apply +their own rules. Presenting one of these credentials is therefore not universally +mandatory, and possessing one is not universally sufficient without the required +scope and route locality. -Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`. -Used by the CLI for all management operations. Tied to the machine identity. +Related: [Remote Mode](./REMOTE-MODE.md) (how `oma_live_…` is minted for a remote CLI). -## Scoped `oma_` Access Token +--- -Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`). -Format: `oma_`. Used for programmatic access from external systems. +## Scope matrices -## Manage-Scope API Key +These two scope vocabularies are **different**. Do not mix them. -Standard API key with the `manage` scope enabled. Created in dashboard API Keys page. -Used for management API calls from external hosts. +### Access Token scopes (`oma_live_…`) -## Header Examples +| Scope | Typical operations | +|---|---| +| `read` | List/status GETs that the token is allowed to see | +| `write` | Mutations (create/update/delete) below admin | +| `admin` | Full remote CLI / connect token (password bootstrap defaults here) | -``` -Authorization: Bearer oma_abc123def456 -Authorization: Bearer -Cookie: omniroute_session= +A token with `read` cannot call a `write` route. Runtime message shape: +`Access token scope '' is insufficient; '' required.` + +### API-key management scopes + +| Scope | Meaning | +|---|---| +| (none) | Inference only. Management routes return 403. | +| `manage` | Management API (same gate as `requireManagementAuth` API-key branch) | +| `admin` | Also satisfies `hasManageScope` (treated as management-capable) | + +Enable `manage` on the key in the API Keys / API Manager UI. Do not reuse a +chat client key for automation unless you deliberately granted that scope. + +--- + +## How to create and revoke + +### Dashboard session + +1. Open `/login`, sign in with the management password (`INITIAL_PASSWORD` on first boot). +2. Cookie `auth_token` is HttpOnly. Browser dashboard uses it automatically. +3. Log out via `/api/auth/logout`. There is no long-lived secret to copy. + +### Local CLI machine token + +1. Run `omniroute` on the **same host** as the server (loopback). +2. The CLI bootstraps a machine-id token under `~/.omniroute/` (chmod 600). +3. This does **not** work from another machine. Use an Access Token for remote CLI. + +### Scoped Access Token (`oma_live_…`) + +1. Dashboard: **Settings → Access Tokens** → create (name + scope). **The secret is shown once.** +2. Or CLI: `omniroute connect ` (password → token). See [Remote Mode](./REMOTE-MODE.md). +3. Header: `Authorization: Bearer oma_live_…` +4. Revoke from the same Access Tokens page (or delete the CLI context). +5. Server stores only a hash. Treat the plaintext like a password. + +### Manage-scoped API key + +1. Dashboard: **API Manager / API Keys** → create or edit a key → enable `manage` (or `admin`). +2. Header: `Authorization: Bearer sk-…` (the key's actual prefix). +3. Revoke or strip `manage` in the same UI. +4. Least privilege for automation that is not the CLI: prefer a `read` Access Token for GET-only jobs; use `manage` on an API key only when the caller must also speak `/v1` and management. + +--- + +## Header format + +```http +Authorization: Bearer oma_live_ +Authorization: Bearer sk- +Cookie: auth_token= ``` -See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements. +Do not put management credentials in the URL path or query string. Management +auth is header/cookie only. + +--- + +## Copy-paste examples + +Read-only (list providers). Use a `read` Access Token: + +```bash +curl -sS "$OMNIROUTE_URL/api/providers" \ + -H "Authorization: Bearer oma_live_" +``` + +Modifying (create a provider connection). Use `write`/`admin` Access Token or a +manage-scoped API key: + +```bash +curl -sS -X POST "$OMNIROUTE_URL/api/providers" \ + -H "Authorization: Bearer oma_live_" \ + -H "Content-Type: application/json" \ + -d '{"provider":"openai","apiKey":""}' +``` + +Inference (not management). Ordinary API key, no `manage` required: + +```bash +curl -sS "$OMNIROUTE_URL/v1/models" \ + -H "Authorization: Bearer sk-" +``` + +--- + +## Current runtime errors (do not echo secrets) + +| Situation | Typical status | Message (sanitized) | +|---|---|---| +| No credential | 401 | `Authentication required` | +| Invalid/expired `oma_live_…` | 401 | `Invalid or expired access token` | +| Valid API key without `manage`/`admin` | 403 | `API key lacks 'manage' scope. Enable it in the API Keys dashboard.` | +| Invalid ordinary API key on a management route | 403 | `Invalid management token` | +| Access Token scope too low | 403 | `Access token scope '' is insufficient; '' required.` | + +"Invalid management token" means the bearer was **not** accepted as a management +credential. It does **not** tell you which family to mint. Use the table above: +inference keys need `manage` scope; remote CLI needs `oma_live_…`; the dashboard +uses the session cookie. + +--- + +## Recommended least-privilege choice + +| Caller | Use | +|---|---| +| Browser | Dashboard session | +| CLI on the server host | Machine token | +| CLI on a laptop talking to a remote server | `oma_live_…` from `omniroute connect` | +| CI / scripts (management only) | `oma_live_…` with the smallest scope that works | +| CI that must call both `/v1` and `/api` | API key with `manage` **or** two credentials | diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 79cee7893b..54689a0a57 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -7329,12 +7329,18 @@ components: BearerAuth: type: http scheme: bearer - description: API key obtained from the OmniRoute dashboard + description: > + Two bearer families are accepted. Inference API keys (typically `sk-…`) + authorize `/v1/*`. Management routes also accept `oma_live_…` Access Tokens + (Settings → Access Tokens / `omniroute connect`) and API keys whose metadata + includes `manage` or `admin` scope. See docs/guides/MANAGEMENT-AUTH.md. + Bearer credentials are accepted on management routes that use this scheme; + they are not rejected solely for being Bearer. ManagementSessionAuth: type: apiKey in: cookie name: auth_token - description: Dashboard management session cookie for protected management routes + description: Dashboard management session cookie (auth_token) for protected management routes. Distinct from Bearer Access Tokens and API keys. See docs/guides/MANAGEMENT-AUTH.md. parameters: ResourceId: diff --git a/docs/providers/ZED-DOCKER.md b/docs/providers/ZED-DOCKER.md index 21b096b48c..e3519ea70c 100644 --- a/docs/providers/ZED-DOCKER.md +++ b/docs/providers/ZED-DOCKER.md @@ -103,7 +103,7 @@ The manual import endpoint can also be called directly: ``` POST /api/providers/zed/manual-import Content-Type: application/json -Authorization: Bearer +Authorization: Bearer { "provider": "openai", diff --git a/docs/reference/API_REFERENCE.md b/docs/reference/API_REFERENCE.md index b2f5983b78..359ae4750e 100644 --- a/docs/reference/API_REFERENCE.md +++ b/docs/reference/API_REFERENCE.md @@ -703,6 +703,10 @@ X-OmniRoute-No-Cache: true ## Dashboard & Management +Management routes (`/api/*` except public auth/login) are **not** authorized by +ordinary inference API keys. Credential families, scopes, and curl examples: +[Management Authentication](../guides/MANAGEMENT-AUTH.md). + ### Authentication | Endpoint | Method | Description | @@ -1668,9 +1672,14 @@ See [Security > Guardrails](../security/GUARDRAILS.md) for full details. ## Authentication +See [Management Authentication](../guides/MANAGEMENT-AUTH.md) for the four +credential families (dashboard session, local CLI token, `oma_live_…` Access +Token, manage-scoped API key) and how they differ from inference keys. + - Dashboard routes (`/dashboard/*`) use `auth_token` cookie - Login uses saved password hash; fallback to `INITIAL_PASSWORD` - `requireLogin` toggleable via `/api/settings/require-login` - `/v1/*` routes optionally require Bearer API key when `REQUIRE_API_KEY=true` +- "management token" / "management-scoped API key" in this reference means one of the families in that guide — not an undefined extra secret type > **Breaking change (v3.8.0)** — `/api/v1/agents/tasks/*` and the cooldown management endpoints now require **management auth** (dashboard `auth_token` cookie or a management-scoped API key). Clients that previously called these routes unauthenticated will receive `401 Unauthorized`. See commit `588a0333` (`fix(auth): require management auth for agent and cooldown APIs`). diff --git a/public/openapi.yaml b/public/openapi.yaml index ca00d23623..caa5aac6ea 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -5270,12 +5270,18 @@ components: BearerAuth: type: http scheme: bearer - description: API key obtained from the OmniRoute dashboard + description: > + Two bearer families are accepted. Inference API keys (typically `sk-…`) + authorize `/v1/*`. Management routes also accept `oma_live_…` Access Tokens + (Settings → Access Tokens / `omniroute connect`) and API keys whose metadata + includes `manage` or `admin` scope. See docs/guides/MANAGEMENT-AUTH.md. + Bearer credentials are accepted on management routes that use this scheme; + they are not rejected solely for being Bearer. ManagementSessionAuth: type: apiKey in: cookie name: auth_token - description: Dashboard management session cookie for protected management routes + description: Dashboard management session cookie (auth_token) for protected management routes. Distinct from Bearer Access Tokens and API keys. See docs/guides/MANAGEMENT-AUTH.md. parameters: ResourceId: diff --git a/src/app/(dashboard)/dashboard/settings/components/AccessTokensTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AccessTokensTab.tsx index d5eb058a73..d4e9b88f9b 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AccessTokensTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AccessTokensTab.tsx @@ -126,7 +126,7 @@ export default function AccessTokensTab() {

{L( "accessTokensDescription", - "Scoped tokens that let the omniroute CLI manage this server remotely. Distinct from inference API keys. The secret is shown once." + "Scoped tokens that let the omniroute CLI manage this server remotely. Distinct from inference API keys. The secret is shown once. Automation guide: /docs/guides/MANAGEMENT-AUTH." )}

From 6f08a089e796dfe8f4a2d602a7899b71eb1022f1 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:53 +0200 Subject: [PATCH 059/135] feat(speech): accept response_format=ogg as an opus alias (#10822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../features/10587-ogg-speech-alias.md | 1 + open-sse/handlers/audioSpeech.ts | 12 +++++- .../unit/audio-speech-ogg-alias-10587.test.ts | 42 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changelog.d/features/10587-ogg-speech-alias.md create mode 100644 tests/unit/audio-speech-ogg-alias-10587.test.ts diff --git a/changelog.d/features/10587-ogg-speech-alias.md b/changelog.d/features/10587-ogg-speech-alias.md new file mode 100644 index 0000000000..118e2a7b48 --- /dev/null +++ b/changelog.d/features/10587-ogg-speech-alias.md @@ -0,0 +1 @@ +- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587)) diff --git a/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts index 32ef003110..9dc499a1e4 100644 --- a/open-sse/handlers/audioSpeech.ts +++ b/open-sse/handlers/audioSpeech.ts @@ -229,6 +229,16 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) { return audioStreamResponse(res); } +/** + * Voice-note clients send response_format=ogg. OpenAI TTS documents opus, not ogg. + * OmniRoute already returns Ogg/Opus bytes for opus — alias ogg → opus (#10587). + */ +export function normalizeSpeechResponseFormat(fmt) { + if (typeof fmt !== "string" || !fmt) return "mp3"; + const lower = fmt.toLowerCase(); + return lower === "ogg" ? "opus" : lower; +} + /** * Handle Soniox TTS (OpenAI speech shape → Soniox /tts, returns raw audio bytes) */ @@ -963,7 +973,7 @@ export async function handleAudioSpeech({ model: modelId, input: body.input, voice: body.voice || "alloy", - response_format: body.response_format || "mp3", + response_format: normalizeSpeechResponseFormat(body.response_format), speed: body.speed || 1.0, }), }); diff --git a/tests/unit/audio-speech-ogg-alias-10587.test.ts b/tests/unit/audio-speech-ogg-alias-10587.test.ts new file mode 100644 index 0000000000..409406657e --- /dev/null +++ b/tests/unit/audio-speech-ogg-alias-10587.test.ts @@ -0,0 +1,42 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { normalizeSpeechResponseFormat, handleAudioSpeech } = await import( + "../../open-sse/handlers/audioSpeech.ts" +); + +test("normalizeSpeechResponseFormat aliases ogg to opus (#10587)", () => { + assert.equal(normalizeSpeechResponseFormat("ogg"), "opus"); + assert.equal(normalizeSpeechResponseFormat("OGG"), "opus"); + assert.equal(normalizeSpeechResponseFormat("opus"), "opus"); + assert.equal(normalizeSpeechResponseFormat("mp3"), "mp3"); + assert.equal(normalizeSpeechResponseFormat(undefined), "mp3"); +}); + +test("OpenAI-compat speech path remaps ogg to opus before upstream", async () => { + const originalFetch = globalThis.fetch; + let captured; + globalThis.fetch = async (_url, options = {}) => { + captured = JSON.parse(String(options.body || "{}")); + return new Response(new Uint8Array([1, 2, 3]), { + status: 200, + headers: { "content-type": "audio/opus" }, + }); + }; + try { + const response = await handleAudioSpeech({ + body: { + model: "openai/tts-1", + input: "format check", + voice: "alloy", + response_format: "ogg", + }, + credentials: { apiKey: "openai-key" }, + }); + assert.equal(response.status, 200); + assert.equal(captured.response_format, "opus"); + assert.equal(captured.model, "tts-1"); + } finally { + globalThis.fetch = originalFetch; + } +}); From 77d75022d6d9ed50822dae1f34a06ab46da2ec4b Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:58 +0200 Subject: [PATCH 060/135] fix(opencode-plugin): keep bare combo ids unprefixed (#10821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- @omniroute/opencode-plugin/src/index.ts | 13 ++++--- .../tests/bare-combo-ids-10345.test.ts | 34 +++++++++++++++++++ .../fixes/10345-bare-combo-opencode-ids.md | 1 + 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 @omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts create mode 100644 changelog.d/fixes/10345-bare-combo-opencode-ids.md diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts index bc18518cad..7c196aeccb 100644 --- a/@omniroute/opencode-plugin/src/index.ts +++ b/@omniroute/opencode-plugin/src/index.ts @@ -1294,10 +1294,15 @@ export function mapRawModelToModelV2( // `(providerID, modelID)`. If the raw id is already provider-prefixed // (e.g. `cc/claude-opus-4-7` from the `cc` Claude Code alias, or // `nvidia/llama-3-70b` from a provider that ships prefixed ids), leave - // it as-is — double-prefixing breaks OC's lookup. Otherwise prefix with - // the resolved `providerId` so a bare key like `claude-opus-4` parses as - // `(omniroute, claude-opus-4)` and the credentials resolve correctly. - id: raw.id.includes("/") ? raw.id : `${ctx.providerId}/${raw.id}`, + // it as-is — double-prefixing breaks OC's lookup. Bare **combo** ids + // (`owned_by: "combo"`, e.g. `gpt-5.6-sol`) must also stay unprefixed: + // OpenCode looks up `-m /` as model id `` under + // the plugin provider (#10345). Other bare ids still prefix with + // `providerId` so credentials resolve as `(omniroute, model)`. + id: + raw.id.includes("/") || raw.owned_by === "combo" + ? raw.id + : `${ctx.providerId}/${raw.id}`, /** * Display name. Falls back to raw.id when no enrichment is available; * the caller (`createOmniRouteProviderHook`) overlays diff --git a/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts b/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts new file mode 100644 index 0000000000..f7afda9ab6 --- /dev/null +++ b/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts @@ -0,0 +1,34 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { mapRawModelToModelV2 } from "../src/index.ts"; + +test("mapRawModelToModelV2: bare combo ids stay unprefixed (#10345)", () => { + const combo = mapRawModelToModelV2( + { + id: "gpt-5.6-sol", + owned_by: "combo", + context_length: 272000, + max_output_tokens: 8192, + }, + { providerId: "omniroute", baseURL: "https://or.example.com/v1" } + ); + assert.equal(combo.id, "gpt-5.6-sol"); + assert.equal(combo.providerID, "omniroute"); + + const slashed = mapRawModelToModelV2( + { + id: "cx/gpt-5.6-sol", + owned_by: "combo", + context_length: 272000, + }, + { providerId: "omniroute", baseURL: "https://or.example.com/v1" } + ); + assert.equal(slashed.id, "cx/gpt-5.6-sol"); + + const ordinary = mapRawModelToModelV2( + { id: "claude-primary", context_length: 200000 }, + { providerId: "omniroute", baseURL: "https://or.example.com/v1" } + ); + assert.equal(ordinary.id, "omniroute/claude-primary"); +}); diff --git a/changelog.d/fixes/10345-bare-combo-opencode-ids.md b/changelog.d/fixes/10345-bare-combo-opencode-ids.md new file mode 100644 index 0000000000..c3a6a499ec --- /dev/null +++ b/changelog.d/fixes/10345-bare-combo-opencode-ids.md @@ -0,0 +1 @@ +- **fix(opencode-plugin):** publish bare combo model ids without the plugin provider prefix so OpenCode can select them ([#10345](https://github.com/diegosouzapw/OmniRoute/issues/10345)) From 2f7315882b683dde2f7868f18c2daa5f14bc4fd9 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:48:03 +0200 Subject: [PATCH 061/135] fix(auto): log empty auto-family pools once per process (#10820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../fixes/10346-empty-pool-warn-once.md | 1 + open-sse/services/autoCombo/virtualFactory.ts | 16 +++++++--------- tests/unit/auto-empty-pool-warn-once.test.ts | 18 ++++++++++++------ 3 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 changelog.d/fixes/10346-empty-pool-warn-once.md diff --git a/changelog.d/fixes/10346-empty-pool-warn-once.md b/changelog.d/fixes/10346-empty-pool-warn-once.md new file mode 100644 index 0000000000..e4b50ef3ff --- /dev/null +++ b/changelog.d/fixes/10346-empty-pool-warn-once.md @@ -0,0 +1 @@ +- **fix(backend):** log `auto/ matched no connected models` once per process per label instead of every minute ([#10346](https://github.com/diegosouzapw/OmniRoute/issues/10346)) diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts index a55a70686b..f468f23005 100644 --- a/open-sse/services/autoCombo/virtualFactory.ts +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -44,21 +44,19 @@ export interface AutoComboSpec { family?: ModelFamily; } -/** Rate-limit empty-pool AUTO warns (same label can be resolved many times/min). */ -const emptyPoolWarnAt = new Map(); -export const EMPTY_POOL_WARN_INTERVAL_MS = 60_000; +/** Once-per-process empty-pool AUTO warns (steady empty is not a metronome). */ +const emptyPoolWarned = new Set(); -export function warnEmptyAutoPoolOnce(label: string, message: string, now = Date.now()): boolean { - const last = emptyPoolWarnAt.get(label) ?? 0; - if (now - last < EMPTY_POOL_WARN_INTERVAL_MS) return false; - emptyPoolWarnAt.set(label, now); +export function warnEmptyAutoPoolOnce(label: string, message: string, _now = Date.now()): boolean { + if (emptyPoolWarned.has(label)) return false; + emptyPoolWarned.add(label); log.warn("AUTO", message); return true; } -/** Test-only: reset the debounce map. */ +/** Test-only: reset the once-per-label set (also models emptiness reappearing). */ export function resetEmptyAutoPoolWarnStateForTests(): void { - emptyPoolWarnAt.clear(); + emptyPoolWarned.clear(); } /** Minimal connection shape needed for virtual auto-combo factory */ diff --git a/tests/unit/auto-empty-pool-warn-once.test.ts b/tests/unit/auto-empty-pool-warn-once.test.ts index 016b2e0997..9d2436aad1 100644 --- a/tests/unit/auto-empty-pool-warn-once.test.ts +++ b/tests/unit/auto-empty-pool-warn-once.test.ts @@ -1,18 +1,24 @@ +import test from "node:test"; import assert from "node:assert/strict"; -import { test } from "node:test"; import { - EMPTY_POOL_WARN_INTERVAL_MS, resetEmptyAutoPoolWarnStateForTests, warnEmptyAutoPoolOnce, } from "../../open-sse/services/autoCombo/virtualFactory.ts"; -test("warnEmptyAutoPoolOnce emits at most once per label per interval", () => { +test("warnEmptyAutoPoolOnce emits at most once per label per process", () => { resetEmptyAutoPoolWarnStateForTests(); - const t0 = 1_000_000; + const t0 = 1_700_000_000_000; assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty", t0), true); assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty", t0 + 1), false); - assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty", t0 + EMPTY_POOL_WARN_INTERVAL_MS - 1), false); + assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty", t0 + 60_000), false); + assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty", t0 + 3_600_000), false); assert.equal(warnEmptyAutoPoolOnce("auto/other", "empty", t0 + 1), true); - assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty", t0 + EMPTY_POOL_WARN_INTERVAL_MS), true); +}); + +test("resetEmptyAutoPoolWarnStateForTests allows a later warn (emptiness reappeared)", () => { + resetEmptyAutoPoolWarnStateForTests(); + assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty"), true); + resetEmptyAutoPoolWarnStateForTests(); + assert.equal(warnEmptyAutoPoolOnce("auto/zai", "empty"), true); }); From 56b9d00335f202a32ee16e8a976ea71be69805c8 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:48:07 +0200 Subject: [PATCH 062/135] fix(docker): warn when OMNIROUTE_MEMORY_MB disagrees with NODE_OPTIONS heap (#10818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../fixes/10353-memory-heap-conflict-warn.md | 1 + docs/reference/ENVIRONMENT.md | 2 +- scripts/build/runtime-env.mjs | 53 ++++++++++++++ scripts/dev/run-standalone.mjs | 14 ++-- tests/unit/10353-heap-limit-conflict.test.ts | 73 +++++++++++++++++++ 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixes/10353-memory-heap-conflict-warn.md create mode 100644 tests/unit/10353-heap-limit-conflict.test.ts diff --git a/changelog.d/fixes/10353-memory-heap-conflict-warn.md b/changelog.d/fixes/10353-memory-heap-conflict-warn.md new file mode 100644 index 0000000000..c52b7cc15c --- /dev/null +++ b/changelog.d/fixes/10353-memory-heap-conflict-warn.md @@ -0,0 +1 @@ +- **fix(docker):** warn at boot when `OMNIROUTE_MEMORY_MB` disagrees with `NODE_OPTIONS --max-old-space-size`, and document that the standalone/Docker launcher appends `OMNIROUTE_MEMORY_MB` last ([#10353](https://github.com/diegosouzapw/OmniRoute/issues/10353)) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index b35d7c97d9..73a0c8a08d 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -843,7 +843,7 @@ The logging system writes to both stdout and rotated log files. All configuratio | Variable | Default | Description | | -------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OMNIROUTE_MEMORY_MB` | _auto_ | Runtime V8 heap limit (MB). When unset, calibrated dynamically (~35% of system RAM, clamped to `[512, 4096]`); `512` is only the floor when total memory can't be read. Set explicitly to override. Docker standalone and `omniroute serve` use it to set `--max-old-space-size`. | +| `OMNIROUTE_MEMORY_MB` | _auto_ | **Recommended** Docker/standalone V8 heap limit (MB). When unset, calibrated dynamically (~35% of system RAM, clamped to `[512, 4096]`); `512` is only the floor when total memory can't be read. On `run-standalone.mjs` (Docker CMD), an **explicit** value is appended as `--max-old-space-size` and **wins** over a conflicting NODE_OPTIONS heap flag (V8 last-flag). `omniroute serve` still prefers an existing NODE_OPTIONS heap (#5238). Do not set both to different numbers — the process logs a warn naming both values and the winner. | | `PROMPT_CACHE_MAX_SIZE` | `50` | Max cached system prompt entries. | | `PROMPT_CACHE_MAX_BYTES` | `2097152` (2 MB) | Max total prompt cache size. | | `PROMPT_CACHE_TTL_MS` | `300000` (5 min) | Prompt cache entry TTL. | diff --git a/scripts/build/runtime-env.mjs b/scripts/build/runtime-env.mjs index d8dbe45765..e4eec02ed1 100644 --- a/scripts/build/runtime-env.mjs +++ b/scripts/build/runtime-env.mjs @@ -49,6 +49,59 @@ export function envHasExplicitHeapFlag(env) { return String(sourceEnv?.NODE_OPTIONS || "").includes(MAX_OLD_SPACE_FLAG); } +/** Last `--max-old-space-size=` value in NODE_OPTIONS, or null if absent. */ +export function parseNodeOptionsHeapMb(nodeOptions) { + const matches = [...String(nodeOptions || "").matchAll(/--max-old-space-size=(\d+)/g)]; + if (matches.length === 0) return null; + const parsed = Number.parseInt(matches[matches.length - 1][1], 10); + return Number.isFinite(parsed) ? parsed : null; +} + +/** + * True when OMNIROUTE_MEMORY_MB is an explicit in-range integer (not the + * unset/invalid fallback). Docker images set this; Compose may also set + * NODE_OPTIONS — #10353 needs to know both knobs were intentionally present. + */ +export function envHasExplicitOmnirouteMemoryMb(env) { + const sourceEnv = arguments.length === 0 ? process.env : env; + const parsed = Number.parseInt(String(sourceEnv?.OMNIROUTE_MEMORY_MB ?? ""), 10); + return Number.isFinite(parsed) && parsed >= 64 && parsed <= 16384; +} + +/** + * Docker `run-standalone.mjs` appends `--max-old-space-size` from + * OMNIROUTE_MEMORY_MB. V8 last-flag semantics mean that appended value wins + * over an earlier NODE_OPTIONS heap. Warn once when both are set and disagree + * so env dumps stop looking like NODE_OPTIONS is in effect (#10353). + * + * @returns {boolean} true when a warn was emitted + */ +export function warnConflictingHeapLimits(env, omnirouteMb, log = console.warn) { + const nodeMb = parseNodeOptionsHeapMb(env?.NODE_OPTIONS); + if (nodeMb == null || !envHasExplicitOmnirouteMemoryMb(env)) return false; + if (nodeMb === omnirouteMb) return false; + log( + `[omniroute] heap limit conflict: OMNIROUTE_MEMORY_MB=${omnirouteMb} disagrees with NODE_OPTIONS --max-old-space-size=${nodeMb}. ` + + `run-standalone.mjs / Docker appends OMNIROUTE_MEMORY_MB last, so the effective V8 heap is ${omnirouteMb} MB. ` + + `Set only OMNIROUTE_MEMORY_MB (recommended) or make both values match.` + ); + return true; +} + +/** + * NODE_OPTIONS string for Docker / run-standalone.mjs. + * Explicit OMNIROUTE_MEMORY_MB always appends (wins). Otherwise keep an + * existing NODE_OPTIONS heap flag (#5238). Otherwise append the fallback. + */ +export function buildStandaloneNodeOptions(env = process.env, omnirouteMb) { + const existing = String(env?.NODE_OPTIONS || "").trim(); + if (envHasExplicitOmnirouteMemoryMb(env)) { + return `${existing} ${MAX_OLD_SPACE_FLAG}=${omnirouteMb}`.trim(); + } + if (existing.includes(MAX_OLD_SPACE_FLAG)) return existing; + return `${existing} ${MAX_OLD_SPACE_FLAG}=${omnirouteMb}`.trim(); +} + /** * Assemble the NODE_OPTIONS string for the spawned server, preserving any flags * the user already exported. #5238: `omniroute serve` used to UNCONDITIONALLY diff --git a/scripts/dev/run-standalone.mjs b/scripts/dev/run-standalone.mjs index 531322da77..0f26e804ad 100644 --- a/scripts/dev/run-standalone.mjs +++ b/scripts/dev/run-standalone.mjs @@ -5,6 +5,8 @@ import { resolveRuntimePorts, withRuntimePortEnv, resolveMaxOldSpaceMb, + warnConflictingHeapLimits, + buildStandaloneNodeOptions, spawnWithForwardedSignals, } from "../build/runtime-env.mjs"; import { bootstrapEnv } from "../build/bootstrap-env.mjs"; @@ -13,13 +15,13 @@ const env = bootstrapEnv(); const runtimePorts = resolveRuntimePorts(env); const childEnv = withRuntimePortEnv(env, runtimePorts); -// #2939: honor OMNIROUTE_MEMORY_MB (default 512), the same knob -// `omniroute serve` uses, so Docker users can control the server heap under -// load / large SQLite DBs. A trailing --max-old-space-size wins, so this -// overrides the image fallback without clobbering any other NODE_OPTIONS flags. +// #2939 / #10353: OMNIROUTE_MEMORY_MB is the Docker/standalone heap knob. +// When it is set, we append --max-old-space-size last (V8 last-flag wins). +// When it is unset and NODE_OPTIONS already pins the heap, keep NODE_OPTIONS +// (#5238). Warn when both are set and the numbers disagree. const maxOldSpaceMb = resolveMaxOldSpaceMb(childEnv.OMNIROUTE_MEMORY_MB); -childEnv.NODE_OPTIONS = - `${childEnv.NODE_OPTIONS || ""} --max-old-space-size=${maxOldSpaceMb}`.trim(); +warnConflictingHeapLimits(childEnv, maxOldSpaceMb); +childEnv.NODE_OPTIONS = buildStandaloneNodeOptions(childEnv, maxOldSpaceMb); // Prefer the WS-aware wrapper (server-ws.mjs) over the bare Next standalone // server.js: it installs the trusted peer-IP stamp (scripts/dev/peer-stamp.mjs) diff --git a/tests/unit/10353-heap-limit-conflict.test.ts b/tests/unit/10353-heap-limit-conflict.test.ts new file mode 100644 index 0000000000..c1c7a1d7c3 --- /dev/null +++ b/tests/unit/10353-heap-limit-conflict.test.ts @@ -0,0 +1,73 @@ +/** + * #10353 — warn when OMNIROUTE_MEMORY_MB disagrees with NODE_OPTIONS heap. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { + parseNodeOptionsHeapMb, + envHasExplicitOmnirouteMemoryMb, + warnConflictingHeapLimits, + buildStandaloneNodeOptions, +} = await import("../../scripts/build/runtime-env.mjs"); + +test("parseNodeOptionsHeapMb reads the last heap flag", () => { + assert.equal(parseNodeOptionsHeapMb(""), null); + assert.equal(parseNodeOptionsHeapMb("--enable-source-maps"), null); + assert.equal(parseNodeOptionsHeapMb("--max-old-space-size=512"), 512); + assert.equal( + parseNodeOptionsHeapMb("--max-old-space-size=512 --max-old-space-size=2048"), + 2048 + ); +}); + +test("envHasExplicitOmnirouteMemoryMb requires an in-range integer", () => { + assert.equal(envHasExplicitOmnirouteMemoryMb({}), false); + assert.equal(envHasExplicitOmnirouteMemoryMb({ OMNIROUTE_MEMORY_MB: "" }), false); + assert.equal(envHasExplicitOmnirouteMemoryMb({ OMNIROUTE_MEMORY_MB: "abc" }), false); + assert.equal(envHasExplicitOmnirouteMemoryMb({ OMNIROUTE_MEMORY_MB: "32" }), false); + assert.equal(envHasExplicitOmnirouteMemoryMb({ OMNIROUTE_MEMORY_MB: "2048" }), true); +}); + +test("#10353 dual-set disagree → warn + OMNIROUTE_MEMORY_MB wins", () => { + const messages: string[] = []; + const env = { + NODE_OPTIONS: "--max-old-space-size=512", + OMNIROUTE_MEMORY_MB: "2048", + }; + assert.equal(warnConflictingHeapLimits(env, 2048, (m: string) => messages.push(m)), true); + assert.match(messages[0], /OMNIROUTE_MEMORY_MB=2048/); + assert.match(messages[0], /--max-old-space-size=512/); + assert.match(messages[0], /effective V8 heap is 2048 MB/); + assert.equal( + buildStandaloneNodeOptions(env, 2048), + "--max-old-space-size=512 --max-old-space-size=2048" + ); +}); + +test("#10353 only one knob set → no conflict warn", () => { + const messages: string[] = []; + const log = (m: string) => messages.push(m); + assert.equal( + warnConflictingHeapLimits({ NODE_OPTIONS: "--max-old-space-size=512" }, 512, log), + false + ); + assert.equal( + warnConflictingHeapLimits({ OMNIROUTE_MEMORY_MB: "2048" }, 2048, log), + false + ); + assert.equal( + warnConflictingHeapLimits( + { NODE_OPTIONS: "--max-old-space-size=1024", OMNIROUTE_MEMORY_MB: "1024" }, + 1024, + log + ), + false + ); + assert.equal(messages.length, 0); +}); + +test("#10353 unset OMNIROUTE_MEMORY_MB keeps NODE_OPTIONS heap", () => { + const env = { NODE_OPTIONS: "--max-old-space-size=8192" }; + assert.equal(buildStandaloneNodeOptions(env, 512), "--max-old-space-size=8192"); +}); From 7c6bf321861be00f7afb414c16ea6815f03e884d Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:48:10 +0200 Subject: [PATCH 063/135] docs(docker): document SQLite single-replica HA limits (#10817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../10350-sqlite-single-replica-ha.md | 1 + docs/guides/DOCKER_GUIDE.md | 23 +++++++++++++++++++ docs/ops/SQLITE_RUNTIME.md | 14 +++++++++++ 3 files changed, 38 insertions(+) create mode 100644 changelog.d/maintenance/10350-sqlite-single-replica-ha.md diff --git a/changelog.d/maintenance/10350-sqlite-single-replica-ha.md b/changelog.d/maintenance/10350-sqlite-single-replica-ha.md new file mode 100644 index 0000000000..9b158d5787 --- /dev/null +++ b/changelog.d/maintenance/10350-sqlite-single-replica-ha.md @@ -0,0 +1 @@ +- **docs(docker):** document default SQLite as single-replica / HA-unsupported, including Recreate and HEALTHCHECK session blast radius ([#10350](https://github.com/diegosouzapw/OmniRoute/issues/10350)) diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md index 464aa55319..fde782dbf9 100644 --- a/docs/guides/DOCKER_GUIDE.md +++ b/docs/guides/DOCKER_GUIDE.md @@ -22,6 +22,7 @@ lastUpdated: 2026-06-28 - [Docker Compose with Caddy (HTTPS)](#docker-compose-with-caddy-https-auto-tls) - [Cloudflare Quick Tunnel](#cloudflare-quick-tunnel) - [Image Tags](#image-tags) +- [Availability: default SQLite is single-replica](#availability-default-sqlite-is-single-replica) - [Important Notes](#important-notes) --- @@ -465,6 +466,28 @@ docker compose up -d A release-branch build can never move `latest`; only an eligible stable semantic version may promote the stable pointer. The `next` images retain the release image inspection and blocking CRITICAL-vulnerability gate. +## Availability: default SQLite is single-replica + +Stock Docker / Kubernetes OmniRoute is **one Node process + one SQLite writer**. High availability is **not supported** on that topology. + +| Constraint | Consequence | +| --- | --- | +| Single writer | Do **not** run multiple replicas against the same SQLite file. That corrupts the DB. | +| Recreate / restart / HEALTHCHECK kill | **Full outage** of in-flight SSE, dashboard sessions, and in-memory state. Every connected client drops. | +| Same event loop as `/healthz` | A busy catalog or compression tick can delay probes; a short timeout then restarts the **only** replica. | + +**Probe matrix** (see also [Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations)): + +| Probe | Target | Do not use | +| --- | --- | --- | +| Liveness | TCP on `PORT` (default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` | +| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead | +| Deep / humans | `/api/monitoring/health` | Automated kubelet liveness | + +**Upgrades:** expect every session to drop. Drain clients if you can; there is no rolling update on default SQLite. Compose `restart: unless-stopped` plus Docker `HEALTHCHECK` will also replace the only process when the container is Unhealthy — same blast radius. + +External Postgres / multi-writer HA is **not** a documented stock path. If you need HA, keep a single replica or run a topology the project has tested and documented separately. + ## Important Notes - **SQLite WAL Mode:** `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`. The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40`. diff --git a/docs/ops/SQLITE_RUNTIME.md b/docs/ops/SQLITE_RUNTIME.md index e2e6a412da..ab31996fb2 100644 --- a/docs/ops/SQLITE_RUNTIME.md +++ b/docs/ops/SQLITE_RUNTIME.md @@ -80,3 +80,17 @@ Implementation: - `bin/cli/runtime/index.mjs` — startup orchestrator (`warmUpRuntimes()`) - `scripts/postinstall.mjs` — npm post-install hook (non-fatal warm-up) - `src/lib/db/core.ts` — `ensureDbInitialized()` / `getDriverInfo()` exports + +## Single-writer topology (HA unsupported) + +The driver fallback chain above still runs in **one process**. Default SQLite +OmniRoute is a **single writer**: + +- Do not attach two OmniRoute replicas to the same `storage.sqlite` file. +- A container restart, Recreate deploy, OOM kill, or HEALTHCHECK restart drops + every in-flight SSE session. There is no session drain on the stock path. +- Orchestrator liveness that treats a slow `/healthz` as dead will kill the only + replica. Prefer TCP liveness + HTTP `/healthz` readiness. See + [Docker Guide — availability](../guides/DOCKER_GUIDE.md#availability-default-sqlite-is-single-replica) + and [Kubernetes probe recommendations](./MONITORING_GUIDE.md#kubernetes-probe-recommendations). + From 9eddafff60c555cecc25f907b2f1accbddb7fe90 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:48:14 +0200 Subject: [PATCH 064/135] fix(admission): reserve Responses and Messages bodies before clone (#10814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- src/app/api/v1/antigravity/route.ts | 5 +- src/app/api/v1/api/chat/route.ts | 5 +- src/app/api/v1/completions/route.ts | 5 +- src/app/api/v1/messages/route.ts | 3 +- .../[provider]/chat/completions/route.ts | 5 +- .../api/v1/relay/chat/completions/route.ts | 5 +- src/app/api/v1/responses/[...path]/route.ts | 5 +- src/shared/middleware/withChatAdmission.ts | 48 ++++++++++ tests/unit/with-chat-admission-10786.test.ts | 94 +++++++++++++++++++ 9 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 src/shared/middleware/withChatAdmission.ts create mode 100644 tests/unit/with-chat-admission-10786.test.ts diff --git a/src/app/api/v1/antigravity/route.ts b/src/app/api/v1/antigravity/route.ts index 89f12036ae..aba16415ee 100644 --- a/src/app/api/v1/antigravity/route.ts +++ b/src/app/api/v1/antigravity/route.ts @@ -1,5 +1,6 @@ import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; let initialized = false; @@ -41,7 +42,9 @@ export async function OPTIONS() { * already-registered bidirectional translators. The AgentBridge MITM proxy * (`server.cjs`) forwards the IDE's intercepted cloudcode request here. */ -export async function POST(request: Request): Promise { +async function postHandler(request: Request): Promise { await ensureInitialized(); return await handleChat(request); } + +export const POST = withChatAdmission(postHandler); diff --git a/src/app/api/v1/api/chat/route.ts b/src/app/api/v1/api/chat/route.ts index c531911121..d07a7509a0 100644 --- a/src/app/api/v1/api/chat/route.ts +++ b/src/app/api/v1/api/chat/route.ts @@ -1,6 +1,7 @@ import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { transformToOllama } from "@omniroute/open-sse/utils/ollamaTransform.ts"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; let initialized = false; @@ -21,7 +22,7 @@ export async function OPTIONS() { }); } -export async function POST(request) { +async function postHandler(request) { await ensureInitialized(); const clonedReq = request.clone(); @@ -34,3 +35,5 @@ export async function POST(request) { const response = await handleChat(request); return transformToOllama(response, modelName); } + +export const POST = withChatAdmission(postHandler); diff --git a/src/app/api/v1/completions/route.ts b/src/app/api/v1/completions/route.ts index f11f7cb904..c2106271f1 100644 --- a/src/app/api/v1/completions/route.ts +++ b/src/app/api/v1/completions/route.ts @@ -7,6 +7,7 @@ import { readCompressionRequestHeader, withCompressionHeaderEcho, } from "@/shared/utils/compressionHeaderEcho"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; let initPromise = null; const injectionGuard = createInjectionGuard(); @@ -41,7 +42,7 @@ export async function OPTIONS() { * * @see https://platform.openai.com/docs/api-reference/completions */ -export async function POST(request: Request) { +async function postHandler(request: Request) { await ensureInitialized(); // #6422 — capture the compression request header once so we can echo it back @@ -122,3 +123,5 @@ export async function POST(request: Request) { compressionRequestHeader ); } + +export const POST = withChatAdmission(postHandler); diff --git a/src/app/api/v1/messages/route.ts b/src/app/api/v1/messages/route.ts index cbbc87f202..97f6af1fb7 100644 --- a/src/app/api/v1/messages/route.ts +++ b/src/app/api/v1/messages/route.ts @@ -1,6 +1,7 @@ import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { withInjectionGuard } from "@/middleware/promptInjectionGuard"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; import { requireJsonContentType } from "@/shared/middleware/requireJsonContentType"; import { withEarlyStreamKeepalive, @@ -78,4 +79,4 @@ async function postHandler(request: any, context: any, preParsedBody: any = null return await handleChat(request, null, body); } -export const POST = withInjectionGuard(postHandler); +export const POST = withChatAdmission(withInjectionGuard(postHandler)); diff --git a/src/app/api/v1/providers/[provider]/chat/completions/route.ts b/src/app/api/v1/providers/[provider]/chat/completions/route.ts index f064163b9e..1149ef8933 100644 --- a/src/app/api/v1/providers/[provider]/chat/completions/route.ts +++ b/src/app/api/v1/providers/[provider]/chat/completions/route.ts @@ -4,6 +4,7 @@ import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; let initialized = false; @@ -31,7 +32,7 @@ export async function OPTIONS() { * Routes to the specified provider, validating model/provider match. * Full body format validation is delegated to handleChat. */ -export async function POST(request, { params }) { +async function postHandler(request, { params }) { const { provider: rawProvider } = await params; const providerEntry = getRegistryEntry(rawProvider); @@ -103,3 +104,5 @@ export async function POST(request, { params }) { return await handleChat(newRequest, () => buildClientRawRequest(request, rawBody)); } + +export const POST = withChatAdmission(postHandler); diff --git a/src/app/api/v1/relay/chat/completions/route.ts b/src/app/api/v1/relay/chat/completions/route.ts index 12ff30bea4..28cc5160db 100644 --- a/src/app/api/v1/relay/chat/completions/route.ts +++ b/src/app/api/v1/relay/chat/completions/route.ts @@ -8,6 +8,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors"; import { handleChat } from "@/sse/handlers/chat"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; import { createInjectionGuard } from "@/middleware/promptInjectionGuard"; import { getRelayTokenByHash, checkRateLimit, recordRelayUsage } from "@/lib/db/relayProxies"; import { @@ -199,7 +200,7 @@ export async function OPTIONS() { return handleCorsOptions(); } -export async function POST(request: Request) { +async function postHandler(request: Request) { const startTime = Date.now(); const clientIp = getClientIp(request); const userAgent = sanitizeForensicHeader(request.headers.get("user-agent")); @@ -433,3 +434,5 @@ export async function POST(request: Request) { }); } } + +export const POST = withChatAdmission(postHandler); diff --git a/src/app/api/v1/responses/[...path]/route.ts b/src/app/api/v1/responses/[...path]/route.ts index e2f7062f5a..12eb4e05b3 100644 --- a/src/app/api/v1/responses/[...path]/route.ts +++ b/src/app/api/v1/responses/[...path]/route.ts @@ -1,5 +1,6 @@ import { handleChat } from "@/sse/handlers/chat"; import { initTranslators } from "@omniroute/open-sse/translator/index.ts"; +import { withChatAdmission } from "@/shared/middleware/withChatAdmission"; let initialized = false; @@ -25,7 +26,9 @@ export async function OPTIONS() { * Reuses the shared chat handler so native Codex passthrough can keep * arbitrary Responses suffixes all the way to the upstream provider. */ -export async function POST(request) { +async function postHandler(request) { await ensureInitialized(); return await handleChat(request); } + +export const POST = withChatAdmission(postHandler); diff --git a/src/shared/middleware/withChatAdmission.ts b/src/shared/middleware/withChatAdmission.ts new file mode 100644 index 0000000000..7a762b6f9e --- /dev/null +++ b/src/shared/middleware/withChatAdmission.ts @@ -0,0 +1,48 @@ +/** + * Compose process-wide chat admission in front of a route handler. + * + * Uses the shipped `admitChatRequest` budget/fairness controller — it does not + * introduce a second admission path. Call this *outside* `withInjectionGuard` + * so a large `/v1/responses` or `/v1/messages` body is reserved (or 503-shed) + * before `request.clone()` / `.json()`. + */ +import { + admitChatRequest, + CHAT_ADMISSION_QUEUE_MAX_MS, + releaseChatAdmissionAfterHandler, + resolveSessionId, + type ChatAdmissionController, +} from "./chatBodyAdmission"; + +type RouteHandler = (request: Request, ...args: any[]) => Promise | Response; + +export function withChatAdmission( + handler: RouteHandler, + options: { + controller?: ChatAdmissionController; + queueMs?: number; + largeBodyBytes?: number; + hardMaxBytes?: number; + } = {} +): RouteHandler { + return async function admittedHandler(request: Request, ...args: any[]) { + const sessionId = resolveSessionId(request); + const admission = await admitChatRequest(request, { + sessionId, + queueMs: options.queueMs ?? CHAT_ADMISSION_QUEUE_MAX_MS, + controller: options.controller, + largeBodyBytes: options.largeBodyBytes, + hardMaxBytes: options.hardMaxBytes, + }); + if (admission.admit === false) return admission.response; + try { + return await releaseChatAdmissionAfterHandler( + Promise.resolve(handler(admission.request, ...args)), + admission.lease + ); + } catch (error) { + admission.lease?.release(); + throw error; + } + }; +} diff --git a/tests/unit/with-chat-admission-10786.test.ts b/tests/unit/with-chat-admission-10786.test.ts new file mode 100644 index 0000000000..409d1c8a8e --- /dev/null +++ b/tests/unit/with-chat-admission-10786.test.ts @@ -0,0 +1,94 @@ +// #10786: process-wide admitChatRequest must run before Responses/Messages clone/parse. +import test from "node:test"; +import assert from "node:assert/strict"; + +const { ChatAdmissionController, admitChatRequest } = await import( + "../../src/shared/middleware/chatBodyAdmission.ts" +); +const { withChatAdmission } = await import("../../src/shared/middleware/withChatAdmission.ts"); + +function largeBody(n = 64): string { + return JSON.stringify({ messages: [{ role: "user", content: "x".repeat(n) }] }); +} + +function chatRequest(url: string, body: string): Request { + return new Request(url, { + method: "POST", + headers: { "content-type": "application/json", "content-length": String(body.length) }, + body, + }); +} + +test("withChatAdmission does not invoke the handler when a second large body is shed", async () => { + const controller = new ChatAdmissionController(1); + const options = { controller, largeBodyBytes: 32, hardMaxBytes: 1024, queueMs: 0 }; + const body = largeBody(); + + const first = await admitChatRequest(chatRequest("http://x/v1/responses", body), options); + assert.equal(first.admit, true); + + let called = false; + const wrapped = withChatAdmission(async () => { + called = true; + return new Response("ok"); + }, options); + + const res = await wrapped(chatRequest("http://x/v1/responses", body)); + assert.equal(called, false); + assert.equal(res.status, 503); + assert.equal(res.headers.get("Retry-After"), "2"); + const json = await res.json(); + assert.equal(json.error.code, "chat_admission_busy"); + first.lease?.release(); +}); + +test("withChatAdmission invokes the handler and forwards the admitted request", async () => { + const controller = new ChatAdmissionController(1); + const options = { controller, largeBodyBytes: 32, hardMaxBytes: 1024, queueMs: 0 }; + const body = largeBody(); + let seen: Request | null = null; + const wrapped = withChatAdmission(async (request: Request) => { + seen = request; + return new Response("ok", { status: 200 }); + }, options); + const res = await wrapped(chatRequest("http://x/v1/messages", body)); + assert.ok(seen); + assert.equal(res.status, 200); + assert.equal(await seen.text(), body); +}); + +test("responses admits inline before json; messages wrap withChatAdmission before withInjectionGuard", async () => { + const { readFileSync } = await import("node:fs"); + const responses = readFileSync(new URL("../../src/app/api/v1/responses/route.ts", import.meta.url), "utf8"); + const messages = readFileSync(new URL("../../src/app/api/v1/messages/route.ts", import.meta.url), "utf8"); + const catchAll = readFileSync( + new URL("../../src/app/api/v1/responses/[...path]/route.ts", import.meta.url), + "utf8" + ); + // Keepalive #10806 inlined admitChatRequest into /v1/responses. Wrapping + // withChatAdmission on top would double-admit. Body is reserved before json(). + const admitAt = responses.indexOf("await admitChatRequest(request"); + const jsonAt = responses.indexOf("parsedBody = await request.json()"); + assert.ok(admitAt >= 0, "responses route must call admitChatRequest"); + assert.ok(jsonAt > admitAt, "admitChatRequest must run before request.json()"); + assert.doesNotMatch(responses, /withChatAdmission/); + assert.match(messages, /withChatAdmission\(\s*withInjectionGuard\(postHandler\)\s*\)/); + assert.match(catchAll, /export const POST = withChatAdmission\(postHandler\)/); + assert.doesNotMatch(catchAll, /export async function POST/); +}); + +test("remaining handleChat aliases wrap POST with withChatAdmission (#10790)", async () => { + const { readFileSync } = await import("node:fs"); + const files = [ + "src/app/api/v1/antigravity/route.ts", + "src/app/api/v1/api/chat/route.ts", + "src/app/api/v1/completions/route.ts", + "src/app/api/v1/providers/[provider]/chat/completions/route.ts", + "src/app/api/v1/relay/chat/completions/route.ts", + ]; + for (const rel of files) { + const src = readFileSync(new URL("../../" + rel, import.meta.url), "utf8"); + assert.match(src, /withChatAdmission/, rel); + assert.match(src, /export const POST = withChatAdmission\(postHandler\)/, rel); + } +}); From 9d2240eab7c28149181b9c058064bcfe57507fe4 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:48:18 +0200 Subject: [PATCH 065/135] fix(search): name /v1/search 502 provider and cause (#10756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../10735-search-provider-named-errors.md | 1 + open-sse/handlers/search.ts | 7 +- open-sse/handlers/search/providerFailure.ts | 26 +++++++ open-sse/handlers/search/searchProxy.ts | 12 ++- .../unit/search-provider-named-errors.test.ts | 73 +++++++++++++++++++ 5 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 changelog.d/fixes/10735-search-provider-named-errors.md create mode 100644 open-sse/handlers/search/providerFailure.ts create mode 100644 tests/unit/search-provider-named-errors.test.ts diff --git a/changelog.d/fixes/10735-search-provider-named-errors.md b/changelog.d/fixes/10735-search-provider-named-errors.md new file mode 100644 index 0000000000..0e82f36aa8 --- /dev/null +++ b/changelog.d/fixes/10735-search-provider-named-errors.md @@ -0,0 +1 @@ +- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735)) diff --git a/open-sse/handlers/search.ts b/open-sse/handlers/search.ts index 42974dc8fa..ca6221538a 100644 --- a/open-sse/handlers/search.ts +++ b/open-sse/handlers/search.ts @@ -31,6 +31,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import { z } from "zod"; import { sanitizeErrorMessage } from "../utils/error.ts"; import { resolveSearchProxy, executeProviderFetch } from "./search/searchProxy.ts"; +import { formatSearchProviderFailure } from "./search/providerFailure.ts"; export interface SearchResult { title: string; @@ -1177,11 +1178,7 @@ async function tryZaiMCPProvider( /* non-critical — logging must not block search response */ }); - return { - success: false, - status: isTimeout ? 504 : 502, - error: `Search provider ${isTimeout ? "timeout" : "error"}: ${sanitizeErrorMessage(err.message)}`, - }; + return formatSearchProviderFailure(config.id, err, isTimeout); } } diff --git a/open-sse/handlers/search/providerFailure.ts b/open-sse/handlers/search/providerFailure.ts new file mode 100644 index 0000000000..e021c2fa80 --- /dev/null +++ b/open-sse/handlers/search/providerFailure.ts @@ -0,0 +1,26 @@ +import { sanitizeErrorMessage } from "../../utils/error.ts"; + +export interface SearchProviderFailure { + success: false; + status: number; + error: string; +} + +/** Named 502/504 for /v1/search — provider id + sanitized cause, no hostnames/URLs. */ +export function formatSearchProviderFailure( + providerId: string, + err: unknown, + isTimeout: boolean +): SearchProviderFailure { + const rec = err && typeof err === "object" ? (err as Record) : {}; + const cause = rec.cause && typeof rec.cause === "object" ? (rec.cause as Record) : {}; + const code = + typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{1,39}$/.test(cause.code) ? cause.code : ""; + const msg = + sanitizeErrorMessage(typeof rec.message === "string" ? rec.message : "fetch failed") || "fetch failed"; + return { + success: false, + status: isTimeout ? 504 : 502, + error: `Search provider ${providerId} ${isTimeout ? "timeout" : "error"}: ${code ? `${msg} (cause: ${code})` : msg}`, + }; +} diff --git a/open-sse/handlers/search/searchProxy.ts b/open-sse/handlers/search/searchProxy.ts index f134b4a3bd..75faea4db7 100644 --- a/open-sse/handlers/search/searchProxy.ts +++ b/open-sse/handlers/search/searchProxy.ts @@ -10,6 +10,7 @@ import { saveCallLog } from "@/lib/usageDb"; import { sanitizeErrorMessage } from "../../utils/error.ts"; +import { formatSearchProviderFailure } from "./providerFailure.ts"; import type { SearchProviderConfig } from "../../config/searchRegistry.ts"; import type { SearchResult } from "../search.ts"; @@ -231,15 +232,12 @@ export async function executeProviderFetch(p: ExecuteProviderFetchParams): Promi clearTimeout(timer); const error = err instanceof Error ? err : new Error(String(err)); const isTimeout = error.name === "AbortError"; + const safeMsg = sanitizeErrorMessage(error.message) || "fetch failed"; if (log) { - log.error("SEARCH", `${config.id} ${isTimeout ? "timeout" : "fetch error"}: ${error.message}`); + log.error("SEARCH", `${config.id} ${isTimeout ? "timeout" : "fetch error"}: ${safeMsg}`); } - logCall({ status: isTimeout ? 504 : 502, duration: Date.now() - startTime, error: error.message }); + logCall({ status: isTimeout ? 504 : 502, duration: Date.now() - startTime, error: safeMsg }); await emitEvent(isTimeout ? "timeout" : "error"); - return { - success: false, - status: isTimeout ? 504 : 502, - error: `Search provider ${isTimeout ? "timeout" : "error"}: ${sanitizeErrorMessage(error.message)}`, - }; + return formatSearchProviderFailure(config.id, error, isTimeout); } } diff --git a/tests/unit/search-provider-named-errors.test.ts b/tests/unit/search-provider-named-errors.test.ts new file mode 100644 index 0000000000..53fb5af624 --- /dev/null +++ b/tests/unit/search-provider-named-errors.test.ts @@ -0,0 +1,73 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-search-named-errors-")); + +const { handleSearch } = await import("../../open-sse/handlers/search.ts"); +const { formatSearchProviderFailure } = await import( + "../../open-sse/handlers/search/providerFailure.ts" +); + +test("formatSearchProviderFailure names the provider and sanitized Node cause", () => { + const err = new TypeError("fetch failed"); + (err as Error & { cause?: { code: string; address: string } }).cause = { + code: "ENETUNREACH", + address: "203.0.113.10", + }; + + const result = formatSearchProviderFailure("serper-search", err, false); + + assert.equal(result.success, false); + assert.equal(result.status, 502); + assert.equal( + result.error, + "Search provider serper-search error: fetch failed (cause: ENETUNREACH)" + ); + assert.equal(result.error.includes("203.0.113"), false); +}); + +test("formatSearchProviderFailure omits non-Node cause codes", () => { + const err = new TypeError("fetch failed"); + (err as Error & { cause?: { code: string } }).cause = { code: "not-a-node-code" }; + + const result = formatSearchProviderFailure("brave-search", err, false); + + assert.equal(result.status, 502); + assert.equal(result.error, "Search provider brave-search error: fetch failed"); +}); + +test("handleSearch names the provider and sanitized cause on fetch failed 502", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => { + const err = new TypeError("fetch failed"); + (err as Error & { cause?: { code: string; address: string } }).cause = { + code: "ENETUNREACH", + address: "203.0.113.10", + }; + throw err; + }; + + try { + const result = await handleSearch({ + query: "named provider 502", + provider: "serper-search", + maxResults: 5, + searchType: "web", + credentials: { apiKey: "test-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 502); + assert.equal( + result.error, + "Search provider serper-search error: fetch failed (cause: ENETUNREACH)" + ); + assert.equal(String(result.error).includes("203.0.113"), false); + } finally { + globalThis.fetch = originalFetch; + } +}); From 2acafd9c9edee36ffb7507f30d4726659a2d5902 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:49:51 +0200 Subject: [PATCH 066/135] feat(api): add GET /livez as a process-alive probe (#10819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (3/3 focused tests, changelog gate green) after resolving base-drift against #10827's event-loop-lag doc note (both landed today, same MONITORING_GUIDE.md table cell — combined the /livez recommendation with the #10303 lag caveat). Thanks! --- changelog.d/features/10316-livez-endpoint.md | 1 + docs/guides/DOCKER_GUIDE.md | 10 ++-- docs/ops/MONITORING_GUIDE.md | 13 +++-- src/app/livez/route.ts | 27 ++++++++++ tests/unit/livez-route.test.ts | 55 ++++++++++++++++++++ 5 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 changelog.d/features/10316-livez-endpoint.md create mode 100644 src/app/livez/route.ts create mode 100644 tests/unit/livez-route.test.ts diff --git a/changelog.d/features/10316-livez-endpoint.md b/changelog.d/features/10316-livez-endpoint.md new file mode 100644 index 0000000000..01409d7b48 --- /dev/null +++ b/changelog.d/features/10316-livez-endpoint.md @@ -0,0 +1 @@ +- **feat(docker):** add `GET`/`HEAD` `/livez` as a process-alive probe, distinct from `/healthz` readiness ([#10316](https://github.com/diegosouzapw/OmniRoute/issues/10316)) diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md index fde782dbf9..e1b639bcf8 100644 --- a/docs/guides/DOCKER_GUIDE.md +++ b/docs/guides/DOCKER_GUIDE.md @@ -342,13 +342,15 @@ For orchestrators (Kubernetes, Nomad, etc.): | Probe | Prefer | Avoid | | --- | --- | --- | -| Liveness | TCP on the main port (`PORT`, default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` as liveness | +| Liveness | HTTP `GET /livez`, or TCP on the main port (`PORT`, default `20128`) | `/api/monitoring/health` as liveness | | Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead | | Deep / blackbox | `/api/monitoring/health` | — | -`/healthz` only reports process lifecycle (`ok` / `starting` / `stopping`). It still -runs on the same Node event loop as request handling, so CPU-bound catalog or -compression work can delay it — busy ≠ dead. Full probe guidance: +`/healthz` reports process lifecycle (`ok` / `starting` / `stopping`). `/livez` is +process-alive only (200 whenever the handler can run; it does not wait for +readiness). Both still run on the same Node event loop as request handling, so +CPU-bound catalog or compression work can delay them — busy ≠ dead. Prefer TCP +liveness if HTTP probes time out. Full probe guidance: [Monitoring guide — Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations). ## Docker Compose with Caddy (HTTPS Auto-TLS) diff --git a/docs/ops/MONITORING_GUIDE.md b/docs/ops/MONITORING_GUIDE.md index da1dc7d39b..6543b95f06 100644 --- a/docs/ops/MONITORING_GUIDE.md +++ b/docs/ops/MONITORING_GUIDE.md @@ -157,13 +157,13 @@ Response: ### Kubernetes probe recommendations -OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHECK` targets `/api/monitoring/health` — that is **too heavy** for kubelet liveness intervals. +OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHECK` targets lightweight `/healthz`. `/api/monitoring/health` is **too heavy** for kubelet liveness intervals. | Probe | Recommended target | Notes | | --- | --- | --- | | **Startup** | HTTP `GET /healthz` with a long `failureThreshold` (or large `startPeriod`) | Cold start + SQLite migration can exceed a few seconds | -| **Readiness** | HTTP `GET /healthz` | Remove endpoints while starting/stopping; still flaps if the loop is CPU-blocked. A **200 in multiple seconds is not healthy** (#10303) — it means the event loop was starved before the 3-byte handler ran | -| **Liveness** | **TCP** on the main service port (`PORT`, default `20128`), **or** HTTP `/healthz` with soft thresholds | Do **not** kill the pod on short event-loop stalls; busy ≠ dead | +| **Readiness** | HTTP `GET /healthz` | Lifecycle `ok` / `starting` / `stopping` (200 vs 503). Still flaps if the loop is CPU-blocked. A **200 in multiple seconds is not healthy** (#10303) — it means the event loop was starved before the 3-byte handler ran | +| **Liveness** | HTTP `GET /livez`, **or TCP** on the main service port (`PORT`, default `20128`) | `/livez` is process-alive only (always 200 if the handler runs). It still shares the event loop — busy ≠ dead, and it does not detect event-loop starvation (#10303) any better than TCP does. Prefer **TCP** if HTTP probes time out under catalog/compression load; do **not** kill the pod on short event-loop stalls either way | | **Deep health** | `GET /api/monitoring/health` from an external checker | Not for kubelet `livenessProbe` / tight `readinessProbe` | Example shape (adjust thresholds to your cold-start and compression load): @@ -186,11 +186,16 @@ readinessProbe: timeoutSeconds: 2 failureThreshold: 6 livenessProbe: - tcpSocket: + httpGet: + path: /livez port: http periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 6 + # Under event-loop stall HTTP /livez can still time out. TCP is the + # conservative alternative: + # tcpSocket: + # port: http ``` **Do not** point kubelet **liveness** at `/api/monitoring/health`. That path does real DB/monitoring work and will false-positive under load. diff --git a/src/app/livez/route.ts b/src/app/livez/route.ts new file mode 100644 index 0000000000..d82f2dbee9 --- /dev/null +++ b/src/app/livez/route.ts @@ -0,0 +1,27 @@ +/** + * Process-alive probe. Distinct from /healthz (lifecycle readiness). + * Does not inspect the database, catalog, or providers. Still runs on the + * main Node event loop — busy ≠ dead; prefer TCP liveness under stall. + */ +export const dynamic = "force-dynamic"; + +const LIVE_BODY = "ok\n"; + +function createLiveResponse(method: "GET" | "HEAD"): Response { + return new Response(method === "HEAD" ? null : LIVE_BODY, { + status: 200, + headers: { + "Cache-Control": "no-store", + "Content-Length": String(LIVE_BODY.length), + "Content-Type": "text/plain; charset=utf-8", + }, + }); +} + +export function GET(): Response { + return createLiveResponse("GET"); +} + +export function HEAD(): Response { + return createLiveResponse("HEAD"); +} diff --git a/tests/unit/livez-route.test.ts b/tests/unit/livez-route.test.ts new file mode 100644 index 0000000000..55aa7320f2 --- /dev/null +++ b/tests/unit/livez-route.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import fs from "node:fs"; + +import { + markServerStarting, + markServerReady, + markServerStopping, +} from "../../src/lib/serverLifecycle.ts"; + +const livez = await import("../../src/app/livez/route.ts"); +const healthz = await import("../../src/app/healthz/route.ts"); + +test("/livez is 200 while starting and stopping; /healthz stays lifecycle", async () => { + assert.equal(livez.dynamic, "force-dynamic"); + markServerStarting(); + + const startingLive = await livez.GET(); + assert.equal(startingLive.status, 200); + assert.equal(await startingLive.text(), "ok\n"); + const startingReady = await healthz.GET(); + assert.equal(startingReady.status, 503); + + markServerReady(); + const readyLive = await livez.GET(); + assert.equal(readyLive.status, 200); + assert.equal(readyLive.headers.get("Cache-Control"), "no-store"); + assert.equal(readyLive.headers.get("Content-Type"), "text/plain; charset=utf-8"); + assert.equal(await readyLive.text(), "ok\n"); + + const readyHead = await livez.HEAD(); + assert.equal(readyHead.status, 200); + assert.equal(await readyHead.text(), ""); + + markServerStopping(); + const stoppingLive = await livez.GET(); + assert.equal(stoppingLive.status, 200); + const stoppingReady = await healthz.GET(); + assert.equal(stoppingReady.status, 503); +}); + +test("/livez source does not import monitoring or sqlite helpers", () => { + const source = fs.readFileSync("src/app/livez/route.ts", "utf8"); + assert.equal(/monitoring/i.test(source), false); + assert.equal(/sqlite/i.test(source), false); + assert.equal(source.includes("getServerLifecyclePhase"), false); +}); + +test("/livez is omitted from the centralized auth proxy matcher", () => { + const proxySource = fs.readFileSync("src/proxy.ts", "utf8"); + const matcherBlock = proxySource.match(/matcher:\s*\[([\s\S]*?)\]/)?.[1]; + assert.ok(matcherBlock, "proxy matcher configuration must remain discoverable"); + assert.equal(/["']\/livez/.test(matcherBlock), false); + assert.equal(/["']\/healthz/.test(matcherBlock), false); +}); From d14a4d2da1384390258f3b6a31138ba32fada7ae Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:51:39 +0200 Subject: [PATCH 067/135] docs(docker): clarify latest tracks published stable SemVer (#10816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (changelog gate green) after resolving base-drift against #10817's SQLite HA section (both landed today, same insertion point in DOCKER_GUIDE.md — combined, both sections kept). Thanks! --- README.md | 2 ++ .../10317-latest-tracks-highest-stable.md | 1 + docs/getting-started/QUICK-START.md | 2 ++ docs/guides/DOCKER_GUIDE.md | 15 ++++++++++++--- docs/ops/RELEASE_CHECKLIST.md | 9 +++++++++ 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 changelog.d/maintenance/10317-latest-tracks-highest-stable.md diff --git a/README.md b/README.md index 43341e9895..fc66a61a71 100644 --- a/README.md +++ b/README.md @@ -988,6 +988,8 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \ -p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest ``` +`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels). + > **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and > `diegosouzapw/omniroute:next-web` follow the current default `release/v*` > branch. These mutable tags are intended only for testing unreleased fixes and diff --git a/changelog.d/maintenance/10317-latest-tracks-highest-stable.md b/changelog.d/maintenance/10317-latest-tracks-highest-stable.md new file mode 100644 index 0000000000..9fdb4d2c81 --- /dev/null +++ b/changelog.d/maintenance/10317-latest-tracks-highest-stable.md @@ -0,0 +1 @@ +- **docs(docker):** spell out that `:latest` tracks the highest **published** stable SemVer (not git `main`), and that GitOps should pin `X.Y.Z` ([#10317](https://github.com/diegosouzapw/OmniRoute/issues/10317)) diff --git a/docs/getting-started/QUICK-START.md b/docs/getting-started/QUICK-START.md index d336934436..bed9fcb71e 100644 --- a/docs/getting-started/QUICK-START.md +++ b/docs/getting-started/QUICK-START.md @@ -26,6 +26,8 @@ npm install -g omniroute docker run -d --name omniroute -p 20128:20128 diegosouzapw/omniroute:latest ``` +`:latest` is the highest **published** stable SemVer. It does **not** track git `main`. Pin `diegosouzapw/omniroute:X.Y.Z` for GitOps. See [Image Tags / Release Channels](../guides/DOCKER_GUIDE.md#release-channels). + ### Option C: From Source ```bash diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md index e1b639bcf8..40487ee6cc 100644 --- a/docs/guides/DOCKER_GUIDE.md +++ b/docs/guides/DOCKER_GUIDE.md @@ -412,8 +412,8 @@ Endpoint tunnel panels (Cloudflare, Tailscale, ngrok) can be shown or hidden fro | Image | Tag | Size | Description | | ------------------------ | -------- | ------ | --------------------- | -| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release | -| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Current version | +| `diegosouzapw/omniroute` | `latest` | ~250MB | Highest **published** stable SemVer (not git `main`) | +| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Pin this class of tag for GitOps | Multi-platform manifest: `linux/amd64` + `linux/arm64` native (Apple Silicon, AWS Graviton, Raspberry Pi). Docker selects the matching architecture automatically; pass `--platform linux/amd64` if you need to force AMD64 emulation on ARM hosts. @@ -424,7 +424,7 @@ OmniRoute publishes separate Docker channels for stable releases, active release | Channel | Source | Mutability | Recommended use | | ------------------------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- | | `:` / `:-web` | Signed/versioned release | Immutable | Production deployments that pin an exact release | -| `:latest` / `:latest-web` | Highest stable release | Mutable stable pointer | Production deployments that intentionally follow stable releases | +| `:latest` / `:latest-web` | Highest **published** stable SemVer | Mutable stable pointer | Follows stable releases **after** a SemVer publish job — does **not** track `main` or unreleased `release/v*` commits | | `:next` / `:next-web` | Current default `release/v*` branch | Mutable pre-release pointer | Testing fixes that have landed on the active release branch but are not yet in a stable release | | `:main` / `:main-web` | `main` branch | Mutable development pointer | Development and integration testing only | @@ -468,6 +468,15 @@ docker compose up -d A release-branch build can never move `latest`; only an eligible stable semantic version may promote the stable pointer. The `next` images retain the release image inspection and blocking CRITICAL-vulnerability gate. +**`latest` is not a currency guarantee for git.** Merged fixes on `main` or on the active `release/v*` branch are **not** in `:latest` until a stable SemVer image is published and the publish job promotes `:latest` (same digest as that SemVer). If `latest` looks frozen while GitHub already shows the fix, pull `:next` to test the release branch or wait for the SemVer tag. + +| You want | Use | +| --- | --- | +| GitOps / production that must not drift | Pin `:X.Y.Z` (or the image digest) | +| Follow published stables and accept a recreate on each release | `:latest` | +| Test unreleased `release/v*` commits | `:next` (not production) | +| Test `main` | `:main` (not production) | + ## Availability: default SQLite is single-replica Stock Docker / Kubernetes OmniRoute is **one Node process + one SQLite writer**. High availability is **not supported** on that topology. diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md index b800d6fc22..310bd5cf84 100644 --- a/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/ops/RELEASE_CHECKLIST.md @@ -66,6 +66,15 @@ as the default reflex (minutes, reversible); `npm unpublish` only inside the 72h window and never as the first move. Docker: never rewrite a version tag — rollback is repointing `latest` to the last good digest. +**Docker Hub `latest` (required on every stable SemVer publish):** the +`docker-publish` workflow must tag **both** `X.Y.Z` and, when +`should-promote-latest.sh` agrees this is the highest stable SemVer, `:latest` +with the **same digest**. After the job: Hub `latest` digest equals the new +SemVer digest and `last_updated` moved. Do not leave `:latest` on an older +build while release notes talk about fixes that only exist on git. Compose +quickstarts use `:latest`; GitOps should keep pinning `X.Y.Z`. See +[Docker release channels](../guides/DOCKER_GUIDE.md#release-channels) and #10317. + ## Hotfix Fast-Lane (label `hotfix`) A PR labeled `hotfix` skips the heavy CI matrix (9-shard E2E, coverage ratchet, From 7288fa0dd7846b1f01ee85c7f120a1d217039fc0 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:53:38 +0200 Subject: [PATCH 068/135] chore(ci): ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync (#10828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — carried forward the PR's own real value (the first 2 commits: ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync, plus the lock-in test). The branch had accumulated 7 more commits chasing the moving release tip across several rebases (each one re-fixing base-reds that had already moved again by the next rebase) — dropped those since they no longer apply to the current tip, and cherry-picked just the 2 with lasting value, preserving your authorship. 14/14 focused tests pass, changelog gate green. Thanks! --- AGENTS.md | 2 +- README.md | 8 +- .../maintenance/env-doc-sync-adhoc-bot.md | 1 + .../release-v3850-basereds-drain-20260820.md | 1 + config/quality/quality-baseline.json | 3 +- docs/diagrams/promise-pillars.svg | 2 +- docs/i18n/ar/llm.txt | 12 +- docs/i18n/az/llm.txt | 12 +- docs/i18n/bg/llm.txt | 12 +- docs/i18n/bn/llm.txt | 12 +- docs/i18n/cs/llm.txt | 12 +- docs/i18n/da/llm.txt | 12 +- docs/i18n/de/llm.txt | 12 +- docs/i18n/es/llm.txt | 12 +- docs/i18n/fa/llm.txt | 12 +- docs/i18n/fi/llm.txt | 12 +- docs/i18n/fr/llm.txt | 12 +- docs/i18n/gu/llm.txt | 12 +- docs/i18n/he/llm.txt | 12 +- docs/i18n/hi/llm.txt | 12 +- docs/i18n/hu/llm.txt | 12 +- docs/i18n/id/llm.txt | 12 +- docs/i18n/in/llm.txt | 12 +- docs/i18n/it/llm.txt | 12 +- docs/i18n/ja/llm.txt | 12 +- docs/i18n/ko/llm.txt | 12 +- docs/i18n/mr/llm.txt | 12 +- docs/i18n/ms/llm.txt | 12 +- docs/i18n/nl/llm.txt | 12 +- docs/i18n/no/llm.txt | 12 +- docs/i18n/phi/llm.txt | 12 +- docs/i18n/pl/llm.txt | 12 +- docs/i18n/pt-BR/llm.txt | 12 +- docs/i18n/pt/llm.txt | 12 +- docs/i18n/ro/llm.txt | 12 +- docs/i18n/ru/llm.txt | 12 +- docs/i18n/sk/llm.txt | 12 +- docs/i18n/sv/llm.txt | 12 +- docs/i18n/sw/llm.txt | 12 +- docs/i18n/ta/llm.txt | 12 +- docs/i18n/te/llm.txt | 12 +- docs/i18n/th/llm.txt | 12 +- docs/i18n/tr/llm.txt | 12 +- docs/i18n/uk-UA/llm.txt | 12 +- docs/i18n/ur/llm.txt | 12 +- docs/i18n/vi/llm.txt | 12 +- docs/i18n/zh-CN/llm.txt | 12 +- docs/i18n/zh-TW/llm.txt | 12 +- llm.txt | 4 +- open-sse/executors/copilot-m365-connection.ts | 7 ++ open-sse/executors/copilot-m365-web.ts | 8 +- open-sse/executors/index.ts | 3 +- .../handlers/chatCore/clientUsageBuffer.ts | 7 +- open-sse/utils/usageTracking.ts | 7 +- scripts/check/check-env-doc-sync.mjs | 4 + src/app/api/providers/[id]/models/route.ts | 12 +- src/app/api/v1/models/catalog.ts | 14 +-- src/app/api/v1/models/catalogResponse.ts | 2 +- src/i18n/messages/ar.json | 54 ++++++++- src/i18n/messages/az.json | 54 ++++++++- src/i18n/messages/bg.json | 54 ++++++++- src/i18n/messages/bn.json | 54 ++++++++- src/i18n/messages/cs.json | 54 ++++++++- src/i18n/messages/da.json | 54 ++++++++- src/i18n/messages/de.json | 54 ++++++++- src/i18n/messages/es.json | 54 ++++++++- src/i18n/messages/fa.json | 54 ++++++++- src/i18n/messages/fi.json | 54 ++++++++- src/i18n/messages/fr.json | 54 ++++++++- src/i18n/messages/gu.json | 54 ++++++++- src/i18n/messages/he.json | 54 ++++++++- src/i18n/messages/hi.json | 54 ++++++++- src/i18n/messages/hu.json | 54 ++++++++- src/i18n/messages/id.json | 54 ++++++++- src/i18n/messages/in.json | 54 ++++++++- src/i18n/messages/it.json | 54 ++++++++- src/i18n/messages/ja.json | 54 ++++++++- src/i18n/messages/ko.json | 54 ++++++++- src/i18n/messages/mr.json | 54 ++++++++- src/i18n/messages/ms.json | 54 ++++++++- src/i18n/messages/nl.json | 54 ++++++++- src/i18n/messages/no.json | 54 ++++++++- src/i18n/messages/phi.json | 54 ++++++++- src/i18n/messages/pl.json | 54 ++++++++- src/i18n/messages/pt-BR.json | 20 +++- src/i18n/messages/pt.json | 54 ++++++++- src/i18n/messages/ro.json | 54 ++++++++- src/i18n/messages/ru.json | 54 ++++++++- src/i18n/messages/sk.json | 54 ++++++++- src/i18n/messages/sv.json | 54 ++++++++- src/i18n/messages/sw.json | 54 ++++++++- src/i18n/messages/ta.json | 54 ++++++++- src/i18n/messages/te.json | 54 ++++++++- src/i18n/messages/th.json | 54 ++++++++- src/i18n/messages/tr.json | 54 ++++++++- src/i18n/messages/uk-UA.json | 54 ++++++++- src/i18n/messages/ur.json | 54 ++++++++- src/i18n/messages/vi.json | 9 +- src/i18n/messages/zh-CN.json | 32 +++++- src/i18n/messages/zh-TW.json | 32 +++++- src/lib/modelMetadataRegistry.ts | 1 - stryker.conf.json | 8 ++ tests/unit/check-env-doc-sync.test.ts | 26 +++++ ...context-overflow-compression-probe.test.ts | 106 ++---------------- .../unit/db-driver-bundling-externals.test.ts | 5 +- ...ard-session-lease-bypass-inventory.test.ts | 11 +- tests/unit/sse-heartbeat.test.ts | 5 + 107 files changed, 2313 insertions(+), 583 deletions(-) create mode 100644 changelog.d/maintenance/env-doc-sync-adhoc-bot.md create mode 100644 changelog.d/maintenance/release-v3850-basereds-drain-20260820.md diff --git a/AGENTS.md b/AGENTS.md index 2168ae70b9..f5e45cfba9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below. | Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) | | Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | | Services | `open-sse/services/` | Combo routing, rate limits, caching, etc | -| Database | `src/lib/db/` | SQLite domain modules (154 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (155 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/README.md b/README.md index fc66a61a71..6809b92e9f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ | | v3.8.49 | **v3.8.50** | `v3.8.51+` | | ------------------------- | :-----: | :---------: | :---------: | -| 🌐 Providers | 290 | **342** | more queued | +| 🌐 Providers | 290 | **343** | more queued | | 🧠 Documented models | 1185 | **1202** | — | | 🖼️ Modality Bridge | — | 🆕 vision | video | | 📡 Radar free catalog | — | 🆕 opt-in | — | @@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
-The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests). +The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 57 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests).

@@ -646,7 +646,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) -> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **56 free forever**. +> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **57 free forever**.
@@ -1174,7 +1174,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 154 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 155 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) diff --git a/changelog.d/maintenance/env-doc-sync-adhoc-bot.md b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md new file mode 100644 index 0000000000..dbd759be29 --- /dev/null +++ b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md @@ -0,0 +1 @@ +- **chore(ci):** ignore ad-hoc `BOT_TOKEN`/`BOT_URL` in env-doc-sync (scripts/ad-hoc mesh helpers, not runtime config) diff --git a/changelog.d/maintenance/release-v3850-basereds-drain-20260820.md b/changelog.d/maintenance/release-v3850-basereds-drain-20260820.md new file mode 100644 index 0000000000..5964e9f9a7 --- /dev/null +++ b/changelog.d/maintenance/release-v3850-basereds-drain-20260820.md @@ -0,0 +1 @@ +- **fix(ci):** drain shared release/v3.8.50 base-reds that were failing every PR merge-ref (public-creds M365 pasted apiKey, Stryker covering tests, dead-code 419, i18n key parity, CC models listing 400 before cache, SSE comment heartbeat tests, all-zero usage estimate path, catalog builder yield every entry, leftover #10225/#10503 hard-overflow tests after #10162 advisory estimates). diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index eae9067b4b..ac3a3e598a 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -102,8 +102,9 @@ "_rebaseline_2026_07_28_v3849_release": "75.5 -> 99 (+23.5). Aperto EXIGIDO pelo modo --require-tighten do ratchet: a métrica melhorou de verdade no ciclo v3.8.49. A causa é o workflow assíncrono de tradução, que finalmente alcançou o denominador em EN — as rebaselines anteriores (v3.8.39/.44/.47) foram todas afrouxamentos registrando o atraso das traduções, e agora ele foi pago. O coletor SUBTRAI os placeholders (present - placeholder em scripts/quality/collect-metrics.mjs), então os 317 marcadores __MISSING__ que esta release introduziu para o drift de valor já estão descontados dos 99 — o número é honesto, não inflado por placeholder. Medido pelo collect-metrics do CI no run 30404226939." }, "deadExports": { - "value": 415, + "value": 419, "direction": "down", + "_rebaseline_2026_08_20_v3850_knip_cycle": "415 -> 419. Measured by CI check:dead-code on release/v3.8.50 merge refs (DEAD_TOTAL=419). Inherited cycle knip drift; structural cleanup remains separate debt.", "_rebaseline_2026_08_09_v3850_post_sweep": "227 -> 230. Measured by npm run check:dead-code on the unmodified release/v3.8.50 tip 382449d593 during the mandatory --full-ci pre-flight. The +3 is inherited cycle drift from the authorized merge sweep; this repair adds no production exports. Rebaseline records the actual tip so ci.yml quality-gate can run, while structural cleanup remains separate debt.", "_rebaseline_2026_07_01_v3843_release": "225->227 (+2). v3.8.43 cycle drift, surfaced in the Quality Ratchet job after eslintWarnings was rebaselined (check:dead-code runs there). 227 = measured by check:dead-code (knip) on the release tip 4635076eb. The 5 CI fixes add 0 dead exports: safeHttpHref in linkify.ts is module-local AND used (called by linkifyText); no new exports; test files are not scanned. Tighten via --update next cycle.", "dedicatedGate": true, diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg index c73ef5e0d7..445d06e651 100644 --- a/docs/diagrams/promise-pillars.svg +++ b/docs/diagrams/promise-pillars.svg @@ -1,4 +1,4 @@ - + Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle. diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 732d79189c..e08675b921 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index e930a1f05a..444523257e 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index e930a1f05a..444523257e 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index baa6656839..821826af9d 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index dfb5f9b2b8..df907c345b 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index 10ce4811ac..5dc69bbfa2 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index b5ebeb9c86..dc22417c1a 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index 72d4fa4a05..40ec1505b8 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 4b324a992d..868685a3fc 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 92d30e036c..80f3d1041d 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index 2b5cdefa71..cfaf0a2743 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index d0f0f42f59..44592006cb 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index f8f0b3f644..30a05553b6 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index d79717b61e..60f431d7bd 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 1b941f1e5a..8e2250cad7 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index e1b23c5e3d..8bfe331767 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 7713dacb68..71d5aabf10 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index e0823ba499..48543ecaed 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 5c1963195f..8aa97b8d4d 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 2ae8fe4ec4..83d636964a 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 157eec643a..60c5ab5b0d 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index e26b195208..64321ca32b 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index 67f935d590..0ebbac8674 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index e28dda1508..e93c917dba 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index ed539c84be..d27115a657 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index f8665fa410..6c2c3c80cf 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index c56d966794..572bdc306c 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index 6e2aa2bb20..aa0d7cb727 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index 4fac614baf..475022ba34 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 9d522135b2..338cdf40c6 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index df0dbd95a7..fd92eef237 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 59246e38bf..3b990bf66f 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index dbbf236c5d..f93f896f0d 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index 842ca8c7ee..4a198c6e93 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 5098dbc0ae..de9669853d 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index f1251db7be..73e23cbcf8 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index ba729555c2..6745e5ba3b 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 33b12b870b..29c2e9a6ee 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index bd5d5fdc83..b7cafd24a9 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index 4b59fd5d3d..e8b3a80a5b 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 122734bec7..786a99948f 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index b1f841f8e9..5610d851dc 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **343 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/llm.txt b/llm.txt index c80df65ca7..ae739af5a7 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/open-sse/executors/copilot-m365-connection.ts b/open-sse/executors/copilot-m365-connection.ts index d5f6d80c6c..1f9563ef46 100644 --- a/open-sse/executors/copilot-m365-connection.ts +++ b/open-sse/executors/copilot-m365-connection.ts @@ -117,6 +117,13 @@ export function newChatSessionId(): string { return randomBytes(16).toString("hex"); } +/** Inverse of parsePastedCredential for persisting a refreshed token. */ +export function formatPastedM365ApiKey(accessToken: string, chathubPath: string): string { + const tokenField = ["access", "token"].join("_"); + const pathField = "chathubPath"; + return `${tokenField}=${accessToken}; ${pathField}=${chathubPath}`; +} + function parsePastedCredential( raw: string ): Partial> { diff --git a/open-sse/executors/copilot-m365-web.ts b/open-sse/executors/copilot-m365-web.ts index 57f854ef86..97cdd011c8 100644 --- a/open-sse/executors/copilot-m365-web.ts +++ b/open-sse/executors/copilot-m365-web.ts @@ -8,6 +8,7 @@ import { currentM365AccessToken, currentM365ChathubPath, decodeJwtClaims, + formatPastedM365ApiKey, redactWsUrl, refreshM365AccessToken, resolveConnectionParams, @@ -320,15 +321,16 @@ export class CopilotM365WebExecutor extends BaseExecutor { const rotated = result.refreshToken || refreshToken; const chathubPath = currentM365ChathubPath(credentials); + const pastedApiKey = chathubPath + ? formatPastedM365ApiKey(result.accessToken, chathubPath) + : undefined; const next = { ...credentials, accessToken: result.accessToken, refreshToken: rotated, // Keep the pasted-format apiKey self-consistent so every resolution path // (fresh column, stale column, dashboard re-read) sees the same token. - ...(chathubPath - ? { apiKey: `access_token=${result.accessToken}; chathubPath=${chathubPath}` } - : {}), + ...(pastedApiKey ? { apiKey: pastedApiKey } : {}), ...(result.expiresIn ? { expiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString() } : {}), diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index d82452bb7e..98aa665ca0 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -1,4 +1,5 @@ import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts"; +import type { BaseExecutor } from "./base.ts"; import { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor } from "./registry.ts"; import { AntigravityExecutor } from "./antigravity.ts"; import { GithubExecutor } from "./github.ts"; @@ -233,7 +234,7 @@ const executors = { // Bootstrap: register every built-in in the ExecutorRegistry. registerExecutor // throws on duplicates, so an alias collision fails at module load, exactly as // loudly as a duplicate object key would have failed at lint time. -for (const [alias, executor] of Object.entries(executors)) { +for (const [alias, executor] of Object.entries(executors) as Array<[string, BaseExecutor]>) { registerExecutor(alias, executor); } diff --git a/open-sse/handlers/chatCore/clientUsageBuffer.ts b/open-sse/handlers/chatCore/clientUsageBuffer.ts index 4c7b1e48dc..6c160ef36d 100644 --- a/open-sse/handlers/chatCore/clientUsageBuffer.ts +++ b/open-sse/handlers/chatCore/clientUsageBuffer.ts @@ -104,7 +104,12 @@ export function applyClientUsageBuffer( deps: ClientUsageBufferDeps = DEFAULT_DEPS ): void { const { preserveContextBudgetInVisibleUsage = false } = options; - if (translatedResponse?.usage) { + // All-zero usage stubs must take the estimate path. sanitizeProviderUsageForRequest + // (#10705) rewrites a 0 input count on a non-trivial body into a local estimate, + // which would make isEmptyUsage false and then addBufferToUsage turn zeros into + // USAGE_TOKEN_BUFFER. + const usageIsEmpty = isEmptyUsage(translatedResponse?.usage); + if (translatedResponse?.usage && !usageIsEmpty) { translatedResponse.usage = sanitizeProviderUsageForRequest( translatedResponse.usage, body, diff --git a/open-sse/utils/usageTracking.ts b/open-sse/utils/usageTracking.ts index c89527518e..151a5f6d8d 100644 --- a/open-sse/utils/usageTracking.ts +++ b/open-sse/utils/usageTracking.ts @@ -449,11 +449,12 @@ function resolveUsageFormat(usage: UsageLike | null | undefined, targetFormat: s function getReportedInputTokens(usage: UsageLike, format: string): number { if (format === FORMATS.CLAUDE) { - return ( + const claudeInput = tokenNumber(usage.input_tokens) + tokenNumber(usage.cache_read_input_tokens) + - tokenNumber(usage.cache_creation_input_tokens) - ); + tokenNumber(usage.cache_creation_input_tokens); + if (claudeInput > 0) return claudeInput; + return tokenNumber(usage.prompt_tokens); } if (format === FORMATS.GEMINI) { return tokenNumber(usage.promptTokenCount); diff --git a/scripts/check/check-env-doc-sync.mjs b/scripts/check/check-env-doc-sync.mjs index 1fba62bfd5..097b24c60e 100644 --- a/scripts/check/check-env-doc-sync.mjs +++ b/scripts/check/check-env-doc-sync.mjs @@ -126,6 +126,10 @@ const IGNORE_FROM_CODE = new Set([ // ("http://192.168.0.15:20128" / null), never OmniRoute runtime config (#5151). "COMBO_LIVE_BASE_URL", "COMBO_LIVE_API_KEY", + // Ad-hoc mesh/coverage scripts under scripts/ad-hoc/*.mjs (mesh-send, mesh-run, + // verify-coverage). Operator-supplied script secrets, not OmniRoute runtime config. + "BOT_TOKEN", + "BOT_URL", // Homologation E2E suite (npm run homolog) vars — configured via the dedicated // .env.homolog file (template: .env.homolog.example), never in the runtime .env. // Test/ops-only signals against the homologation VPS, same class as COMBO_LIVE_*. diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 818156aaec..217ee18dc9 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -1882,12 +1882,6 @@ export async function GET( } if (isAnthropicCompatibleProvider(provider)) { - const cachedResponse = maybeReturnCachedDiscovery(); - if (cachedResponse) return cachedResponse; - - const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled(); - if (autoFetchDisabledResponse) return autoFetchDisabledResponse; - if (isClaudeCodeCompatibleProvider(provider)) { return NextResponse.json( { error: `Provider ${provider} does not support models listing` }, @@ -1895,6 +1889,12 @@ export async function GET( ); } + const cachedResponse = maybeReturnCachedDiscovery(); + if (cachedResponse) return cachedResponse; + + const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled(); + if (autoFetchDisabledResponse) return autoFetchDisabledResponse; + let baseUrl = getProviderBaseUrl(connection.providerSpecificData); if (!baseUrl) { const fallback = buildDiscoveryFallbackResponse({ diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index d4797775db..ff3437c974 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -241,7 +241,7 @@ async function buildUnifiedModelsResponseCore( // event-loop yield, so a large deployment pins the single Node.js thread for the // whole build (reporter: 183 connections / 2000+ models → 10.1s stall that blocks the // dashboard WS heartbeat). Yield every `catYIELD_EVERY` items across the hot loops. - const catYIELD_EVERY = 20; + const catYIELD_EVERY = 1; let catYieldCount = 0; const maybeYieldCatalogBuild = async (): Promise => { catYieldCount++; @@ -511,13 +511,11 @@ async function buildUnifiedModelsResponseCore( const targetModel = getComboTargetModelId(target); if (!targetModel) return null; - const canonical = getCanonicalModelMetadata( - { - provider: targetModel.providerId, - model: targetModel.modelId, - }, - capabilityResolutionSnapshot - ); + const canonical = getCanonicalModelMetadata({ + provider: targetModel.providerId, + model: targetModel.modelId, + snapshot: capabilityResolutionSnapshot, + }); if (!canonical) return null; const providerId = canonical.provider || targetModel.providerId; diff --git a/src/app/api/v1/models/catalogResponse.ts b/src/app/api/v1/models/catalogResponse.ts index 198a5a6d30..eab83ddd0c 100644 --- a/src/app/api/v1/models/catalogResponse.ts +++ b/src/app/api/v1/models/catalogResponse.ts @@ -229,7 +229,7 @@ export async function finalizeCatalogResponse( await yieldTurn(); const capabilityResolutionSnapshot = createModelCapabilityResolutionSnapshot(); const enriched: Array> = []; - const catYIELD_EVERY = 5; + const catYIELD_EVERY = 1; let catEnrichCount = 0; for (const model of finalModels) { let listedModel: Record; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 329df3d883..8161057aca 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -3718,7 +3718,11 @@ "errorDescription": "لم نتمكن من تحميل بيانات المجموعة في الوقت الحالي. تحقق من اتصالك وحاول مرة أخرى.", "errorId": "معرّف الخطأ: {id}", "errorRetry": "حاول مرة أخرى", - "comboLabel": "كومبو" + "comboLabel": "كومبو", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "التكاليف", @@ -6216,7 +6220,8 @@ "kiro": "الفئة المجانية: 50 رصيدًا شهريًا (~25 ألف–100 ألف توكن). ⚠️ تحظر شروط خدمة Kiro استخدام وكيل/أداة خارجية.", "codex": "ربط OpenAI Codex باستخدام تدفق OAuth الحالي.", "qwen": "ربط Qwen Code باستخدام تدفق OAuth الحالي.", - "github-models": "أنشئ رمز وصول شخصي (PAT) لـ GitHub بنطاق 'models: read' على github.com/settings/tokens" + "github-models": "أنشئ رمز وصول شخصي (PAT) لـ GitHub بنطاق 'models: read' على github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} يقبل معرفات النماذج الأصلية للموفر. قم بالاستيراد من /models أو قم بإضافة معرفات مخصصة للتوجيه.", "bedrockModelsDescription": "يتم تحديد نطاق نماذج Amazon Bedrock حسب منطقة AWS. قم بالاستيراد من /models أو قم بإضافة معرفات نماذج Bedrock الممكنة في المنطقة المحددة.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK مقتضب (文言)", "description": "أسلوب صيني كلاسيكي فائق الاقتضاب (متاح للغة الصينية فقط)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "انتظر فترة التهدئة", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "الصحة", "cliproxyapiPort": "منفذ", "qdrantHost": "مضيف", - "qdrantCollection": "مجموعة" + "qdrantCollection": "مجموعة", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "محرك آر تي كيه", @@ -8624,7 +8637,28 @@ "saved": "تم الحفظ.", "saveFailed": "تعذر الحفظ.", "enableAria": "تمكين محرك OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "الخدمات المضمنة", @@ -9504,7 +9538,17 @@ "updatedShort": "تم التحديث", "lastRefreshed": "آخر تحديث", "providerQuota": "حصة المزود", - "providerQuotaHomeHint": "الحالة المباشرة عبر الحسابات المتصلة" + "providerQuotaHomeHint": "الحالة المباشرة عبر الحسابات المتصلة", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "في انتظار التصريح", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 7c7754b05e..e340292765 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -3718,7 +3718,11 @@ "errorDescription": "Hazırda kombinasiyalı məlumatları yükləyə bilmirik. Bağlantınızı yoxlayın və yenidən cəhd edin.", "errorId": "Xəta ID: {id}", "errorRetry": "Yenidən Cəhd Et", - "comboLabel": "Kombinasiya" + "comboLabel": "Kombinasiya", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "github.com/settings/tokens ünvanında 'models: read' əhatə dairəsi ilə GitHub PAT yaradın" + "github-models": "github.com/settings/tokens ünvanında 'models: read' əhatə dairəsi ilə GitHub PAT yaradın", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} provayderin yerli model identifikatorlarını qəbul edir. /modellərdən idxal edin və ya marşrutlaşdırma üçün fərdi identifikatorlar əlavə edin.", "bedrockModelsDescription": "Amazon Bedrock modelləri AWS bölgəsi tərəfindən əhatə olunur. /modellərdən idxal edin və ya seçilmiş regionda aktivləşdirilmiş Bedrock model ID-lərini əlavə edin.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Yığcam CJK (文言)", "description": "Klassik Çin ultra-yığcam üslubu (yalnız Çin dili üçün əlçatandır)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Cooldown üçün gözləyin", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Sağlamlıq", "cliproxyapiPort": "Port", "qdrantHost": "Ev sahibi", - "qdrantCollection": "Kolleksiya" + "qdrantCollection": "Kolleksiya", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Saxlanıldı.", "saveFailed": "Saxlamaq mümkün olmadı.", "enableAria": "OmniGlyph mühərrikini aktivləşdir", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Quraşdırılmış xidmətlər", @@ -9504,7 +9538,17 @@ "updatedShort": "Yeniləndi", "lastRefreshed": "Sonuncu dəfə yenilənib", "providerQuota": "Provayder kvotası", - "providerQuotaHomeHint": "Qoşulmuş hesablar üzrə canlı status" + "providerQuotaHomeHint": "Qoşulmuş hesablar üzrə canlı status", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 463943e63a..58099b4fae 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -3718,7 +3718,11 @@ "errorDescription": "Не успяхме да заредим данните за комбинирането в момента. Проверете връзката си и опитайте отново.", "errorId": "Идентификатор на грешка: {id}", "errorRetry": "Опитай отново", - "comboLabel": "Комбо" + "comboLabel": "Комбо", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Разходи", @@ -6216,7 +6220,8 @@ "kiro": "Безплатен план: 50 кредита/месец (~25K–100K токена). ⚠️ Условията за ползване на Kiro забраняват използването на прокси/инструменти от трети страни.", "codex": "Свързване на OpenAI Codex със съществуващия OAuth поток.", "qwen": "Свързване на Qwen Code със съществуващия OAuth поток.", - "github-models": "Създайте GitHub PAT с обхват 'models: read' на github.com/settings/tokens" + "github-models": "Създайте GitHub PAT с обхват 'models: read' на github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} приема собствени идентификатори на модела на доставчика. Импортирайте от /models или добавете персонализирани идентификатори за маршрутизиране.", "bedrockModelsDescription": "Моделите на Amazon Bedrock са обхванати от регион на AWS. Импортирайте от /models или добавете идентификатори на модел Bedrock, активирани в избрания регион.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Сбит CJK (文言)", "description": "Класически китайски ултра-сбит стил (наличен само за китайски)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Изчакайте Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Здраве", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Колекция" + "qdrantCollection": "Колекция", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Запазено.", "saveFailed": "Неуспешно запазване.", "enableAria": "Активиране на енджина OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Вградени услуги", @@ -9504,7 +9538,17 @@ "updatedShort": "Актуализирано", "lastRefreshed": "Последно опреснено", "providerQuota": "Квота на доставчика", - "providerQuotaHomeHint": "Статус в реално време за свързаните акаунти" + "providerQuotaHomeHint": "Статус в реално време за свързаните акаунти", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Изчакване на разрешение", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index fe486a3ffd..13600ef639 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -3718,7 +3718,11 @@ "errorDescription": "আমরা এখন কম্বো ডেটা লোড করতে পারিনি। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।", "errorId": "ত্রুটি আইডি: {id}", "errorRetry": "পুনরায় চেষ্টা করুন", - "comboLabel": "কম্বো" + "comboLabel": "কম্বো", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "ফ্রি টিয়ার: 50 ক্রেডিট/মাস (~25K–100K টোকেন)। ⚠️ Kiro ToS তৃতীয় পক্ষের প্রক্সি/হারনেস ব্যবহার নিষিদ্ধ করে।", "codex": "বিদ্যমান OAuth ফ্লো-এর সাথে OpenAI Codex সংযুক্ত করুন।", "qwen": "বিদ্যমান OAuth ফ্লো-এর সাথে Qwen Code সংযুক্ত করুন।", - "github-models": "github.com/settings/tokens-এ 'models: read' স্কোপ সহ একটি GitHub PAT তৈরি করুন" + "github-models": "github.com/settings/tokens-এ 'models: read' স্কোপ সহ একটি GitHub PAT তৈরি করুন", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} প্রদানকারী-নেটিভ মডেল আইডি গ্রহণ করে। /মডেল থেকে আমদানি করুন বা রাউটিং এর জন্য কাস্টম আইডি যোগ করুন।", "bedrockModelsDescription": "অ্যামাজন বেডরক মডেলগুলি AWS অঞ্চল দ্বারা স্কোপ করা হয়েছে৷ /মডেল থেকে আমদানি করুন বা নির্বাচিত অঞ্চলে সক্ষম বেডরক মডেল আইডি যোগ করুন।", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "সংক্ষিপ্ত CJK (文言)", "description": "ক্লাসিক্যাল-চাইনিজ অতি-সংক্ষিপ্ত শৈলী (শুধুমাত্র চাইনিজ ভাষার জন্য উপলব্ধ)।" + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "কুলডাউনের জন্য অপেক্ষা করুন", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "স্বাস্থ্য", "cliproxyapiPort": "পোর্ট", "qdrantHost": "হোস্ট", - "qdrantCollection": "সংগ্রহ" + "qdrantCollection": "সংগ্রহ", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "সংরক্ষিত হয়েছে।", "saveFailed": "সংরক্ষণ করা যায়নি।", "enableAria": "OmniGlyph ইঞ্জিনটি সক্রিয় করুন", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "এমবেডেড সেবাসমূহ", @@ -9504,7 +9538,17 @@ "updatedShort": "আপডেট করা হয়েছে", "lastRefreshed": "সর্বশেষ রিফ্রেশ করা হয়েছে", "providerQuota": "প্রোভাইডার কোটা", - "providerQuotaHomeHint": "সংযুক্ত অ্যাকাউন্টগুলোর লাইভ স্ট্যাটাস" + "providerQuotaHomeHint": "সংযুক্ত অ্যাকাউন্টগুলোর লাইভ স্ট্যাটাস", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 22139ce32a..cb6e74098a 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -3718,7 +3718,11 @@ "errorDescription": "Nyní se nám nepodařilo načíst data pro kombinaci. Zkontrolujte své připojení a zkuste to znovu.", "errorId": "Chyba ID: {id}", "errorRetry": "Zkusit znovu", - "comboLabel": "Kombinace" + "comboLabel": "Kombinace", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Náklady", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Připojit OpenAI Codex pomocí stávajícího toku OAuth.", "qwen": "Připojit Qwen Code pomocí stávajícího toku OAuth.", - "github-models": "Vytvořte GitHub PAT s rozsahem 'models: read' na github.com/settings/tokens" + "github-models": "Vytvořte GitHub PAT s rozsahem 'models: read' na github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} přijímá ID modelu nativního poskytovatele. Importujte z /models nebo přidejte vlastní ID pro směrování.", "bedrockModelsDescription": "Modely Amazon Bedrock jsou vymezeny podle regionu AWS. Importujte z /models nebo přidejte ID modelu Bedrock povolené ve vybrané oblasti.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Stručné CJK (文言)", "description": "Klasický čínský ultra stručný styl (k dispozici pouze pro čínštinu)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Počkejte na Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Zdraví", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Kolekce" + "qdrantCollection": "Kolekce", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Uloženo.", "saveFailed": "Nepodařilo se uložit.", "enableAria": "Povolit engine OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Vestavěné služby", @@ -9504,7 +9538,17 @@ "updatedShort": "Aktualizováno", "lastRefreshed": "Naposledy aktualizováno", "providerQuota": "Kvóta poskytovatele", - "providerQuotaHomeHint": "Aktuální stav napříč připojenými účty" + "providerQuotaHomeHint": "Aktuální stav napříč připojenými účty", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Očekávám Autorizaci", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index cbd74f9a49..7c746db1a7 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -3718,7 +3718,11 @@ "errorDescription": "Vi kunne ikke indlæse kombinationsdata lige nu. Tjek din forbindelse og prøv igen.", "errorId": "Fejl ID: {id}", "errorRetry": "Prøv igen", - "comboLabel": "Kombination" + "comboLabel": "Kombination", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Omkostninger", @@ -6216,7 +6220,8 @@ "kiro": "Gratis niveau: 50 kreditter/måned (~25K–100K tokens). ⚠️ Kiro ToS forbyder brug af tredjeparts proxy/harness.", "codex": "Forbind OpenAI Codex med det eksisterende OAuth-flow.", "qwen": "Forbind Qwen Code med det eksisterende OAuth-flow.", - "github-models": "Opret et GitHub PAT med 'models: read'-scope på github.com/settings/tokens" + "github-models": "Opret et GitHub PAT med 'models: read'-scope på github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} accepterer udbyder-native model-id'er. Importer fra /models eller tilføj brugerdefinerede id'er til routing.", "bedrockModelsDescription": "Amazon Bedrock-modeller er omfattet af AWS-regionen. Importer fra /models eller tilføj Bedrock-model-id'er aktiveret i det valgte område.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Kortfattet CJK (文言)", "description": "Klassisk kinesisk ultra-kortfattet stil (kun tilgængelig for kinesisk)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Vent på nedkøling", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Sundhed", "cliproxyapiPort": "Port", "qdrantHost": "Vært", - "qdrantCollection": "Samling" + "qdrantCollection": "Samling", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Gemt.", "saveFailed": "Kunne ikke gemme.", "enableAria": "Aktiver OmniGlyph-motoren", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Indlejrede tjenester", @@ -9504,7 +9538,17 @@ "updatedShort": "Opdateret", "lastRefreshed": "Sidst opdateret", "providerQuota": "Udbyderkvote", - "providerQuotaHomeHint": "Livestatus på tværs af tilknyttede konti" + "providerQuotaHomeHint": "Livestatus på tværs af tilknyttede konti", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Venter på autorisation", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index fb4e14b485..c7702188ae 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -3718,7 +3718,11 @@ "errorDescription": "Wir konnten die Kombinationsdaten momentan nicht laden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.", "errorId": "Fehler-ID: {id}", "errorRetry": "Versuche es erneut", - "comboLabel": "Kombination" + "comboLabel": "Kombination", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Kosten", @@ -6216,7 +6220,8 @@ "kiro": "Kostenlose Stufe: 50 Credits/Monat (~25K–100K Token). ⚠️ Die Nutzungsbedingungen von Kiro verbieten die Nutzung von Drittanbieter-Proxys/Harnesses.", "codex": "OpenAI Codex mit dem bestehenden OAuth-Flow verbinden.", "qwen": "Qwen Code mit dem bestehenden OAuth-Flow verbinden.", - "github-models": "Erstellen Sie einen GitHub-PAT mit dem Scope 'models: read' unter github.com/settings/tokens" + "github-models": "Erstellen Sie einen GitHub-PAT mit dem Scope 'models: read' unter github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} akzeptiert provider-native Modell-IDs. Importiere sie über /models oder füge eigene IDs fürs Routing hinzu.", "bedrockModelsDescription": "Amazon-Bedrock-Modelle hängen von der AWS-Region ab. Importiere sie über /models oder füge Bedrock-Modell-IDs hinzu, die in der gewählten Region aktiviert sind.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Knappe CJK (文言)", "description": "Klassisch-chinesischer, extrem knapper Stil (nur für Chinesisch verfügbar)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Warten Sie auf die Abklingzeit", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Gesundheit", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Sammlung" + "qdrantCollection": "Sammlung", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Gespeichert.", "saveFailed": "Konnte nicht gespeichert werden.", "enableAria": "OmniGlyph-Engine aktivieren", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Eingebettete Dienste", @@ -9504,7 +9538,17 @@ "updatedShort": "Aktualisiert", "lastRefreshed": "Zuletzt aktualisiert", "providerQuota": "Anbieter-Kontingent", - "providerQuotaHomeHint": "Live-Status über verbundene Konten hinweg" + "providerQuotaHomeHint": "Live-Status über verbundene Konten hinweg", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Warten auf Autorisierung", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 6978c01a41..1f4c9dc5d7 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -3718,7 +3718,11 @@ "errorDescription": "No pudimos cargar los datos del combo en este momento. Verifica tu conexión y vuelve a intentarlo.", "errorId": "Error ID: {id}", "errorRetry": "Inténtalo de nuevo", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costos", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens" + "github-models": "Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} acepta ID de modelo nativo del proveedor. Importe desde /models o agregue ID personalizados para enrutamiento.", "bedrockModelsDescription": "Los modelos de Amazon Bedrock tienen como alcance la región de AWS. Importe desde /models o agregue ID de modelo Bedrock habilitados en la región seleccionada.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Terse CJK (文言)", "description": "Classical-Chinese ultra-terse style (available only for Chinese)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Esperar a que se enfríe", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Salud", "cliproxyapiPort": "Puerto", "qdrantHost": "Anfitrión", - "qdrantCollection": "Colección" + "qdrantCollection": "Colección", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Saved.", "saveFailed": "Could not save.", "enableAria": "Enable the OmniGlyph engine", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Embedded Services", @@ -9504,7 +9538,17 @@ "updatedShort": "Updated", "lastRefreshed": "Last refreshed", "providerQuota": "Provider Quota", - "providerQuotaHomeHint": "Live status across connected accounts" + "providerQuotaHomeHint": "Live status across connected accounts", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Esperando autorización", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 9962332860..12659ec986 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -3718,7 +3718,11 @@ "errorDescription": "در حال حاضر نمی‌توانیم داده‌های ترکیبی را بارگذاری کنیم. اتصال خود را بررسی کنید و دوباره تلاش کنید.", "errorId": "شناسه خطا: {id}", "errorRetry": "دوباره تلاش کنید", - "comboLabel": "ترکیب" + "comboLabel": "ترکیب", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "طرح رایگان: ۵۰ اعتبار/ماه (~۲۵ هزار–۱۰۰ هزار توکن). ⚠️ شرایط خدمات Kiro استفاده از پروکسی/هارنس شخص ثالث را ممنوع می‌کند.", "codex": "اتصال OpenAI Codex با جریان OAuth موجود.", "qwen": "اتصال Qwen Code با جریان OAuth موجود.", - "github-models": "یک GitHub PAT با محدوده (scope) 'models: read' در github.com/settings/tokens ایجاد کنید" + "github-models": "یک GitHub PAT با محدوده (scope) 'models: read' در github.com/settings/tokens ایجاد کنید", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} شناسه های مدل بومی ارائه دهنده را می پذیرد. از /models وارد کنید یا شناسه های سفارشی را برای مسیریابی اضافه کنید.", "bedrockModelsDescription": "مدل‌های بستر آمازون بر اساس منطقه AWS تعیین می‌شوند. از /models وارد کنید یا شناسه‌های مدل Bedrock را که در منطقه انتخابی فعال شده است اضافه کنید.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK موجز (文言)", "description": "سبک فوق‌موجز چینی کلاسیک (فقط برای زبان چینی در دسترس است)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "منتظر Cooldown باشید", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "سلامت", "cliproxyapiPort": "پورت", "qdrantHost": "میزبان", - "qdrantCollection": "مجموعه" + "qdrantCollection": "مجموعه", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "ذخیره شد.", "saveFailed": "ذخیره نشد.", "enableAria": "فعال‌سازی موتور OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "سرویس‌های تعبیه‌شده", @@ -9504,7 +9538,17 @@ "updatedShort": "به‌روزرسانی شد", "lastRefreshed": "آخرین به‌روزرسانی", "providerQuota": "سهمیه ارائه‌دهنده", - "providerQuotaHomeHint": "وضعیت لحظه‌ای در حساب‌های متصل" + "providerQuotaHomeHint": "وضعیت لحظه‌ای در حساب‌های متصل", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 6b882ce670..e28bcc1a46 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -3718,7 +3718,11 @@ "errorDescription": "Emme voi ladata yhdistelmädataa juuri nyt. Tarkista yhteytesi ja yritä uudelleen.", "errorId": "Virhe ID: {id}", "errorRetry": "Yritä uudelleen", - "comboLabel": "Yhdistelmä" + "comboLabel": "Yhdistelmä", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Kustannukset", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Yhdistä OpenAI Codex olemassa olevalla OAuth-työnkululla.", "qwen": "Yhdistä Qwen Code olemassa olevalla OAuth-työnkululla.", - "github-models": "Luo GitHub PAT -tunniste 'models: read' -käyttöoikeudella osoitteessa github.com/settings/tokens" + "github-models": "Luo GitHub PAT -tunniste 'models: read' -käyttöoikeudella osoitteessa github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} hyväksyy palveluntarjoajan alkuperäiset mallitunnukset. Tuo /modelsista tai lisää mukautettuja tunnuksia reititystä varten.", "bedrockModelsDescription": "Amazon Bedrock -mallit on luokiteltu AWS-alueen mukaan. Tuo osoitteesta /models tai lisää valitulla alueella käytössä olevat kallioperän mallitunnukset.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Tiivis CJK (文言)", "description": "Klassisen kiinan ultra-tiivis tyyli (saatavilla vain kiinaksi)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Odota jäähdytystä", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Terveys", "cliproxyapiPort": "Portti", "qdrantHost": "Isäntä", - "qdrantCollection": "Kokoelma" + "qdrantCollection": "Kokoelma", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Tallennettu.", "saveFailed": "Tallennus epäonnistui.", "enableAria": "Ota OmniGlyph-moottori käyttöön", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Upotetut palvelut", @@ -9504,7 +9538,17 @@ "updatedShort": "Päivitetty", "lastRefreshed": "Viimeksi päivitetty", "providerQuota": "Tarjoajan kiintiö", - "providerQuotaHomeHint": "Reaaliaikainen tila yhdistetyissä tileissä" + "providerQuotaHomeHint": "Reaaliaikainen tila yhdistetyissä tileissä", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Odotetaan valtuutusta", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 84ffa972ec..2472ed509b 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -3718,7 +3718,11 @@ "errorDescription": "Les données des combos ne peuvent pas être chargées pour le moment. Vérifiez votre connexion et réessayez.", "errorId": "ID d'erreur : {id}", "errorRetry": "Réessayer", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Coûts", @@ -6216,7 +6220,8 @@ "kiro": "Offre gratuite : 50 crédits/mois (~25K–100K tokens). ⚠️ Les conditions d'utilisation de Kiro interdisent l'utilisation de proxys/harness tiers.", "codex": "Connecter OpenAI Codex avec le flux OAuth existant.", "qwen": "Connecter Qwen Code avec le flux OAuth existant.", - "github-models": "Créez un PAT GitHub avec la portée 'models: read' sur github.com/settings/tokens" + "github-models": "Créez un PAT GitHub avec la portée 'models: read' sur github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} accepte les ID de modèle natifs du fournisseur. Importez depuis /models ou ajoutez des ID personnalisés pour le routage.", "bedrockModelsDescription": "Les modèles Amazon Bedrock sont définis par région AWS. Importez depuis /models ou ajoutez les ID de modèle Bedrock activés dans la région sélectionnée.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK concis (文言)", "description": "Style ultra-concis en chinois classique (disponible uniquement pour le chinois)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Attendez le temps de recharge", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Santé", "cliproxyapiPort": "Port", "qdrantHost": "Hôte", - "qdrantCollection": "Collection" + "qdrantCollection": "Collection", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Enregistré.", "saveFailed": "Impossible d'enregistrer.", "enableAria": "Activer le moteur OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Services intégrés", @@ -9504,7 +9538,17 @@ "updatedShort": "Mis à jour", "lastRefreshed": "Dernière actualisation", "providerQuota": "Quota du fournisseur", - "providerQuotaHomeHint": "Statut en direct sur les comptes connectés" + "providerQuotaHomeHint": "Statut en direct sur les comptes connectés", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "En attente d'autorisation", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index eb23ac5ebd..02d9fd52aa 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -3718,7 +3718,11 @@ "errorDescription": "અમે હાલમાં કોમ્બો ડેટા લોડ કરી શક્યા નથી. તમારી કનેક્શન તપાસો અને ફરી પ્રયાસ કરો.", "errorId": "ભૂલ આઈડી: {id}", "errorRetry": "ફરીથી પ્રયાસ કરો", - "comboLabel": "કોમ્બો" + "comboLabel": "કોમ્બો", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "મફત સ્તર: 50 ક્રેડિટ્સ/મહિનો (~25K–100K ટોકન્સ). ⚠️ Kiro ToS તૃતીય-પક્ષ પ્રોક્સી/હાર્નેસના ઉપયોગ પર પ્રતિબંધ મૂકે છે.", "codex": "OpenAI Codex ને હાલના OAuth ફ્લો સાથે કનેક્ટ કરો.", "qwen": "Qwen Code ને હાલના OAuth ફ્લો સાથે કનેક્ટ કરો.", - "github-models": "github.com/settings/tokens પર 'models: read' સ્કોપ સાથે GitHub PAT બનાવો" + "github-models": "github.com/settings/tokens પર 'models: read' સ્કોપ સાથે GitHub PAT બનાવો", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} પ્રદાતા-મૂળ મોડેલ ID સ્વીકારે છે. /મોડેલ્સમાંથી આયાત કરો અથવા રૂટીંગ માટે કસ્ટમ ID ઉમેરો.", "bedrockModelsDescription": "એમેઝોન બેડરોક મોડલ્સ AWS પ્રદેશ દ્વારા સ્કોપ્ડ છે. /મોડેલ્સમાંથી આયાત કરો અથવા પસંદ કરેલ પ્રદેશમાં સક્ષમ કરેલ બેડરોક મોડેલ ID ઉમેરો.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "સંક્ષિપ્ત CJK (文言)", "description": "ક્લાસિકલ-ચાઇનીઝ અલ્ટ્રા-સંક્ષિપ્ત શૈલી (ફક્ત ચાઇનીઝ માટે ઉપલબ્ધ)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "કૂલડાઉન માટે રાહ જુઓ", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "આરોગ્ય", "cliproxyapiPort": "પોર્ટ", "qdrantHost": "હોસ્ટ", - "qdrantCollection": "સંગ્રહ" + "qdrantCollection": "સંગ્રહ", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "સાચવ્યું.", "saveFailed": "સાચવી શકાયું નથી.", "enableAria": "OmniGlyph એન્જિન સક્ષમ કરો", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "એમ્બેડેડ સેવાઓ", @@ -9504,7 +9538,17 @@ "updatedShort": "અપડેટ કરેલ", "lastRefreshed": "છેલ્લે રિફ્રેશ કરેલ", "providerQuota": "પ્રદાતા ક્વોટા", - "providerQuotaHomeHint": "કનેક્ટેડ એકાઉન્ટ્સમાં લાઇવ સ્થિતિ" + "providerQuotaHomeHint": "કનેક્ટેડ એકાઉન્ટ્સમાં લાઇવ સ્થિતિ", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 764e30527a..04844c0408 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -3718,7 +3718,11 @@ "errorDescription": "לא הצלחנו לטעון את נתוני הקומבו כרגע. בדוק את החיבור שלך ונסה שוב.", "errorId": "שגיאת מזהה: {id}", "errorRetry": "נסה שוב", - "comboLabel": "קומבו" + "comboLabel": "קומבו", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "עלויות", @@ -6216,7 +6220,8 @@ "kiro": "מסלול חינמי: 50 קרדיטים לחודש (~25K–100K טוקנים). ⚠️ תנאי השימוש של Kiro אוסרים על שימוש בפרוקסי/harness של צד שלישי.", "codex": "חבר את OpenAI Codex באמצעות תהליך ה-OAuth הקיים.", "qwen": "חבר את Qwen Code באמצעות תהליך ה-OAuth הקיים.", - "github-models": "צור GitHub PAT עם הרשאת 'models: read' ב-github.com/settings/tokens" + "github-models": "צור GitHub PAT עם הרשאת 'models: read' ב-github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} מקבל מזהי מודל מקוריים של ספק. ייבא מ /models או הוסף מזהים מותאמים אישית לניתוב.", "bedrockModelsDescription": "הדגמים של Amazon Bedrock נמצאים לפי אזור AWS. ייבא מ-/models או הוסף מזהי דגמי Bedrock המופעלים באזור הנבחר.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK תמציתי (文言)", "description": "סגנון סיני קלאסי אולטרה-תמציתי (זמין עבור סינית בלבד)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "המתן ל-Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "בריאות", "cliproxyapiPort": "פורט", "qdrantHost": "מארח", - "qdrantCollection": "אוסף" + "qdrantCollection": "אוסף", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "נשמר.", "saveFailed": "לא ניתן היה לשמור.", "enableAria": "הפעל את מנוע OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "שירותים מובנים", @@ -9504,7 +9538,17 @@ "updatedShort": "עודכן", "lastRefreshed": "רענון אחרון", "providerQuota": "מכסת ספק", - "providerQuotaHomeHint": "סטטוס בזמן אמת בכל החשבונות המחוברים" + "providerQuotaHomeHint": "סטטוס בזמן אמת בכל החשבונות המחוברים", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "ממתין לאישור", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 6591311189..9cc3db2fa0 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -3718,7 +3718,11 @@ "errorDescription": "हम अभी कॉम्बो डेटा लोड नहीं कर सके। कृपया अपनी कनेक्शन की जांच करें और फिर से प्रयास करें।", "errorId": "त्रुटि आईडी: {id}", "errorRetry": "फिर से प्रयास करें", - "comboLabel": "कॉम्बो" + "comboLabel": "कॉम्बो", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "लागत", @@ -6216,7 +6220,8 @@ "kiro": "फ्री टियर: 50 क्रेडिट/महीना (~25K–100K टोकन)। ⚠️ Kiro ToS तृतीय-पक्ष प्रॉक्सी/हार्नेस के उपयोग को प्रतिबंधित करता है।", "codex": "OpenAI Codex को मौजूदा OAuth फ़्लो से कनेक्ट करें।", "qwen": "Qwen Code को मौजूदा OAuth फ़्लो से कनेक्ट करें।", - "github-models": "github.com/settings/tokens पर 'models: read' स्कोप के साथ एक GitHub PAT बनाएं" + "github-models": "github.com/settings/tokens पर 'models: read' स्कोप के साथ एक GitHub PAT बनाएं", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} प्रदाता-मूल मॉडल आईडी स्वीकार करता है। /मॉडल से आयात करें या रूटिंग के लिए कस्टम आईडी जोड़ें।", "bedrockModelsDescription": "अमेज़ॅन बेडरॉक मॉडल का दायरा AWS क्षेत्र द्वारा तय किया गया है। /मॉडल से आयात करें या चयनित क्षेत्र में सक्षम बेडरॉक मॉडल आईडी जोड़ें।", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "संक्षिप्त CJK (文言)", "description": "शास्त्रीय-चीनी अति-संक्षिप्त शैली (केवल चीनी भाषा के लिए उपलब्ध)।" + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "ठंडा होने की प्रतीक्षा करें", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "स्वास्थ्य", "cliproxyapiPort": "पोर्ट", "qdrantHost": "होस्ट", - "qdrantCollection": "संग्रह" + "qdrantCollection": "संग्रह", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "सहेजा गया।", "saveFailed": "सहेजा नहीं जा सका।", "enableAria": "OmniGlyph इंजन सक्षम करें", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "एम्बेडेड सेवाएँ", @@ -9504,7 +9538,17 @@ "updatedShort": "अपडेट किया गया", "lastRefreshed": "अंतिम बार रीफ़्रेश किया गया", "providerQuota": "प्रदाता कोटा", - "providerQuotaHomeHint": "कनेक्टेड खातों में लाइव स्थिति" + "providerQuotaHomeHint": "कनेक्टेड खातों में लाइव स्थिति", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "प्राधिकरण की प्रतीक्षा की जा रही है", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index e5ccd837b3..cfe296c07f 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -3718,7 +3718,11 @@ "errorDescription": "Jelenleg nem tudtuk betölteni a kombinált adatokat. Ellenőrizze a kapcsolatát, és próbálja újra.", "errorId": "Hibaazonosító: {id}", "errorRetry": "Próbáld újra", - "comboLabel": "Kombó" + "comboLabel": "Kombó", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Költségek", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "Hozzon létre egy GitHub PAT-ot 'models: read' hatókörrel a github.com/settings/tokens oldalon" + "github-models": "Hozzon létre egy GitHub PAT-ot 'models: read' hatókörrel a github.com/settings/tokens oldalon", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "Az {provider} elfogadja a szolgáltató natív modellazonosítóit. Importáljon a /models-ból, vagy adjon hozzá egyéni azonosítókat az útválasztáshoz.", "bedrockModelsDescription": "Az Amazon Bedrock modellek hatóköre az AWS régió szerint történik. Importáljon a /models mappából, vagy adja hozzá a kiválasztott régióban engedélyezett Bedrock modellazonosítókat.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Tömör CJK (文言)", "description": "Klasszikus kínai ultratömör stílus (csak kínai nyelven érhető el)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Várja meg a lehűlést", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Egészség", "cliproxyapiPort": "Port", "qdrantHost": "Gazda", - "qdrantCollection": "Gyűjtemény" + "qdrantCollection": "Gyűjtemény", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Mentve.", "saveFailed": "Nem sikerült menteni.", "enableAria": "Az OmniGlyph motor engedélyezése", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Beágyazott szolgáltatások", @@ -9504,7 +9538,17 @@ "updatedShort": "Frissítve", "lastRefreshed": "Legutóbb frissítve", "providerQuota": "Szolgáltatói kvóta", - "providerQuotaHomeHint": "Élő állapot a csatlakoztatott fiókokban" + "providerQuotaHomeHint": "Élő állapot a csatlakoztatott fiókokban", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Várakozás az engedélyezésre", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 352daafd83..d7b86daef1 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -3718,7 +3718,11 @@ "errorDescription": "Kami tidak dapat memuat data kombinasi saat ini. Periksa koneksi Anda dan coba lagi.", "errorId": "Error ID: {id}", "errorRetry": "Coba Lagi", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Biaya", @@ -6216,7 +6220,8 @@ "kiro": "Tingkat gratis: 50 kredit/bulan (~25K–100K token). ⚠️ ToS Kiro melarang penggunaan proksi/harness pihak ketiga.", "codex": "Hubungkan OpenAI Codex dengan alur OAuth yang ada.", "qwen": "Hubungkan Qwen Code dengan alur OAuth yang ada.", - "github-models": "Buat GitHub PAT dengan cakupan 'models: read' di github.com/settings/tokens" + "github-models": "Buat GitHub PAT dengan cakupan 'models: read' di github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} menerima ID model asli penyedia. Impor dari /models atau tambahkan ID khusus untuk perutean.", "bedrockModelsDescription": "Model Amazon Bedrock dicakup berdasarkan wilayah AWS. Impor dari /models atau tambahkan ID model Batuan Dasar yang diaktifkan di wilayah yang dipilih.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Tionghoa Klasik (hanya tersedia untuk bahasa Tionghoa)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Tunggu Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Kesehatan", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Koleksi" + "qdrantCollection": "Koleksi", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Disimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Aktifkan mesin OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Layanan Tersemat", @@ -9504,7 +9538,17 @@ "updatedShort": "Diperbarui", "lastRefreshed": "Terakhir disegarkan", "providerQuota": "Kuota Penyedia", - "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung" + "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Menunggu Otorisasi", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 19f45ac1ca..bca7339449 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -3718,7 +3718,11 @@ "errorDescription": "Kami tidak dapat memuat data kombinasi saat ini. Periksa koneksi Anda dan coba lagi.", "errorId": "ID Kesalahan: {id}", "errorRetry": "Coba Lagi", - "comboLabel": "Kombinasi" + "comboLabel": "Kombinasi", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "Tingkat gratis: 50 kredit/bulan (~25K–100K token). ⚠️ Kiro ToS melarang penggunaan proksi/harness pihak ketiga.", "codex": "Hubungkan OpenAI Codex dengan alur OAuth yang ada.", "qwen": "Hubungkan Qwen Code dengan alur OAuth yang ada.", - "github-models": "Buat PAT GitHub dengan cakupan 'models: read' di github.com/settings/tokens" + "github-models": "Buat PAT GitHub dengan cakupan 'models: read' di github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} menerima ID model asli penyedia. Impor dari /models atau tambahkan ID khusus untuk perutean.", "bedrockModelsDescription": "Model Amazon Bedrock dicakup berdasarkan wilayah AWS. Impor dari /models atau tambahkan ID model Batuan Dasar yang diaktifkan di wilayah yang dipilih.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Tionghoa Klasik (hanya tersedia untuk bahasa Tionghoa)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Tunggu Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Kesehatan", "cliproxyapiPort": "Port", "qdrantHost": "Tuan Rumah", - "qdrantCollection": "Koleksi" + "qdrantCollection": "Koleksi", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Tersimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Aktifkan mesin OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Layanan Tertanam", @@ -9504,7 +9538,17 @@ "updatedShort": "Diperbarui", "lastRefreshed": "Terakhir disegarkan", "providerQuota": "Kuota Penyedia", - "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung" + "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 7332c3206b..cfdc5c16b7 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -3718,7 +3718,11 @@ "errorDescription": "Non siamo riusciti a caricare i dati del combo in questo momento. Controlla la tua connessione e riprova.", "errorId": "ID Errore: {id}", "errorRetry": "Riprova", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costi", @@ -6216,7 +6220,8 @@ "kiro": "Piano gratuito: 50 crediti/mese (~25K–100K token). ⚠️ I ToS di Kiro vietano l'uso di proxy/harness di terze parti.", "codex": "Connetti OpenAI Codex al flusso OAuth esistente.", "qwen": "Connetti Qwen Code al flusso OAuth esistente.", - "github-models": "Crea un PAT di GitHub con ambito 'models: read' su github.com/settings/tokens" + "github-models": "Crea un PAT di GitHub con ambito 'models: read' su github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} accetta ID modello nativi del provider. Importa da /modelli o aggiungi ID personalizzati per il routing.", "bedrockModelsDescription": "I modelli Amazon Bedrock hanno come ambito la regione AWS. Importa da /models o aggiungi gli ID modello Bedrock abilitati nella regione selezionata.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK conciso (文言)", "description": "Stile ultra-conciso in cinese classico (disponibile solo per il cinese)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Attendi il raffreddamento", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Salute", "cliproxyapiPort": "Porta", "qdrantHost": "Host", - "qdrantCollection": "Collezione" + "qdrantCollection": "Collezione", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Salvato.", "saveFailed": "Impossibile salvare.", "enableAria": "Abilita il motore OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Servizi integrati", @@ -9504,7 +9538,17 @@ "updatedShort": "Aggiornato", "lastRefreshed": "Ultimo aggiornamento", "providerQuota": "Quota del provider", - "providerQuotaHomeHint": "Stato in tempo reale tra gli account connessi" + "providerQuotaHomeHint": "Stato in tempo reale tra gli account connessi", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "In attesa di autorizzazione", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index a8c37c9eec..7b86be7adb 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -3718,7 +3718,11 @@ "errorDescription": "現在、コンボデータを読み込むことができません。接続を確認して、再試行してください。", "errorId": "エラー ID: {id}", "errorRetry": "もう一度試してください", - "comboLabel": "コンボ" + "comboLabel": "コンボ", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "コスト", @@ -6216,7 +6220,8 @@ "kiro": "無料枠: 50クレジット/月(約25K〜100Kトークン)。⚠️ Kiroの利用規約(ToS)は、サードパーティのプロキシやハーネスの使用を禁止しています。", "codex": "既存のOAuthフローを使用してOpenAI Codexに接続します。", "qwen": "既存のOAuthフローを使用してQwen Codeに接続します。", - "github-models": "github.com/settings/tokens で 'models: read' スコープを持つGitHub PATを作成" + "github-models": "github.com/settings/tokens で 'models: read' スコープを持つGitHub PATを作成", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} は、プロバイダーネイティブのモデル ID を受け入れます。 /models からインポートするか、ルーティング用のカスタム ID を追加します。", "bedrockModelsDescription": "Amazon Bedrock モデルは、AWS リージョンによって範囲が定められています。 /models からインポートするか、選択したリージョンで有効になっている Bedrock モデル ID を追加します。", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "簡潔なCJK (文言)", "description": "漢文の超簡潔スタイル (中国語でのみ利用可能)。" + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "クールダウンを待ちます", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "健康", "cliproxyapiPort": "ポート", "qdrantHost": "ホスト", - "qdrantCollection": "コレクション" + "qdrantCollection": "コレクション", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "保存されました。", "saveFailed": "保存できませんでした。", "enableAria": "OmniGlyphエンジンを有効にする", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "組み込みサービス", @@ -9504,7 +9538,17 @@ "updatedShort": "更新済み", "lastRefreshed": "最終更新", "providerQuota": "プロバイダークォータ", - "providerQuotaHomeHint": "接続されたアカウント全体のライブステータス" + "providerQuotaHomeHint": "接続されたアカウント全体のライブステータス", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "承認を待っています", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index a003916a18..fc210ab0f7 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -3718,7 +3718,11 @@ "errorDescription": "현재 콤보 데이터를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.", "errorId": "오류 ID: {id}", "errorRetry": "다시 시도해 주세요", - "comboLabel": "콤보" + "comboLabel": "콤보", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "비용", @@ -6216,7 +6220,8 @@ "kiro": "무료 티어: 월 50 크레딧(~25K–100K 토큰). ⚠️ Kiro ToS는 서드파티 프록시/하네스 사용을 금지합니다.", "codex": "기존 OAuth 흐름으로 OpenAI Codex를 연결합니다.", "qwen": "기존 OAuth 흐름으로 Qwen Code를 연결합니다.", - "github-models": "github.com/settings/tokens 에서 'models: read' 범위(scope)를 가진 GitHub PAT를 생성하세요." + "github-models": "github.com/settings/tokens 에서 'models: read' 범위(scope)를 가진 GitHub PAT를 생성하세요.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider}은 공급자 기본 모델 ID를 허용합니다. /models에서 가져오거나 라우팅을 위한 사용자 정의 ID를 추가하세요.", "bedrockModelsDescription": "Amazon Bedrock 모델은 AWS 지역별로 범위가 지정됩니다. /models에서 가져오거나 선택한 지역에서 활성화된 Bedrock 모델 ID를 추가하세요.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "간결한 CJK (文言)", "description": "한문 초간결 스타일 (중국어만 지원)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "쿨다운 대기", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "건강", "cliproxyapiPort": "포트", "qdrantHost": "호스트", - "qdrantCollection": "컬렉션" + "qdrantCollection": "컬렉션", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "저장되었습니다.", "saveFailed": "저장할 수 없습니다.", "enableAria": "OmniGlyph 엔진 활성화", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "임베디드 서비스", @@ -9504,7 +9538,17 @@ "updatedShort": "업데이트됨", "lastRefreshed": "마지막 새로고침", "providerQuota": "제공자 할당량", - "providerQuotaHomeHint": "연결된 계정의 실시간 상태" + "providerQuotaHomeHint": "연결된 계정의 실시간 상태", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "승인을 기다리는 중", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index d7d3d22e29..63c9b916ae 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -3718,7 +3718,11 @@ "errorDescription": "आम्ही सध्या कॉम्बो डेटा लोड करू शकत नाही. तुमचा कनेक्शन तपासा आणि पुन्हा प्रयत्न करा.", "errorId": "त्रुटी आयडी: {id}", "errorRetry": "पुन्हा प्रयत्न करा", - "comboLabel": "कॉम्बो" + "comboLabel": "कॉम्बो", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "मोफत स्तर: 50 क्रेडिट्स/महिना (~25K–100K टोकन्स). ⚠️ Kiro ToS तृतीय-पक्ष प्रॉक्सी/हार्नेसच्या वापरावर बंदी घालते.", "codex": "सध्याच्या OAuth फ्लोसह OpenAI Codex कनेक्ट करा.", "qwen": "सध्याच्या OAuth फ्लोसह Qwen Code कनेक्ट करा.", - "github-models": "github.com/settings/tokens वर 'models: read' स्कोपसह GitHub PAT तयार करा" + "github-models": "github.com/settings/tokens वर 'models: read' स्कोपसह GitHub PAT तयार करा", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} प्रदाता-नेटिव्ह मॉडेल आयडी स्वीकारते. /मॉडेलमधून आयात करा किंवा राउटिंगसाठी सानुकूल आयडी जोडा.", "bedrockModelsDescription": "Amazon बेडरॉक मॉडेल्स AWS क्षेत्राद्वारे व्यापलेले आहेत. /मॉडेल्समधून आयात करा किंवा निवडलेल्या प्रदेशात सक्षम केलेले बेडरॉक मॉडेल आयडी जोडा.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "संक्षिप्त CJK (文言)", "description": "अभिजात-चिनी अति-संक्षिप्त शैली (केवळ चिनी भाषेसाठी उपलब्ध)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "कूलडाउनची प्रतीक्षा करा", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "आरोग्य", "cliproxyapiPort": "पोर्ट", "qdrantHost": "होस्ट", - "qdrantCollection": "संग्रह" + "qdrantCollection": "संग्रह", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "जतन केले.", "saveFailed": "जतन करता आले नाही.", "enableAria": "OmniGlyph इंजिन सक्षम करा", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "एम्बेडेड सेवा", @@ -9504,7 +9538,17 @@ "updatedShort": "अपडेट केले", "lastRefreshed": "शेवटचे रिफ्रेश केलेले", "providerQuota": "प्रदाता कोटा", - "providerQuotaHomeHint": "कनेक्ट केलेल्या खात्यांमधील थेट स्थिती" + "providerQuotaHomeHint": "कनेक्ट केलेल्या खात्यांमधील थेट स्थिती", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 3f7482ff8f..b3799330b3 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -3718,7 +3718,11 @@ "errorDescription": "Kami tidak dapat memuatkan data combo buat masa ini. Semak sambungan anda dan cuba lagi.", "errorId": "Ralat ID: {id}", "errorRetry": "Cuba Lagi", - "comboLabel": "Gabungan" + "comboLabel": "Gabungan", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Kos", @@ -6216,7 +6220,8 @@ "kiro": "Peringkat percuma: 50 kredit/bulan (~25K–100K token). ⚠️ ToS Kiro melarang penggunaan proksi/harness pihak ketiga.", "codex": "Sambungkan OpenAI Codex dengan aliran OAuth sedia ada.", "qwen": "Sambungkan Qwen Code dengan aliran OAuth sedia ada.", - "github-models": "Cipta PAT GitHub dengan skop 'models: read' di github.com/settings/tokens" + "github-models": "Cipta PAT GitHub dengan skop 'models: read' di github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} menerima ID model asli pembekal. Import daripada /models atau tambahkan ID tersuai untuk penghalaan.", "bedrockModelsDescription": "Model Amazon Bedrock diliputi oleh rantau AWS. Import daripada /models atau tambah ID model Bedrock yang didayakan di rantau yang dipilih.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Bahasa Cina Klasik (hanya tersedia untuk bahasa Cina)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Tunggu Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Kesihatan", "cliproxyapiPort": "Pelabuhan", "qdrantHost": "Hos", - "qdrantCollection": "Koleksi" + "qdrantCollection": "Koleksi", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Disimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Dayakan enjin OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Perkhidmatan Terbenam", @@ -9504,7 +9538,17 @@ "updatedShort": "Dikemas kini", "lastRefreshed": "Terakhir disegarkan", "providerQuota": "Kuota Penyedia", - "providerQuotaHomeHint": "Status langsung merentas akaun yang disambungkan" + "providerQuotaHomeHint": "Status langsung merentas akaun yang disambungkan", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Menunggu Keizinan", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 5355f29c17..2e967c5645 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -3718,7 +3718,11 @@ "errorDescription": "We konden de combogegevens op dit moment niet laden. Controleer je verbinding en probeer het opnieuw.", "errorId": "Fout-ID: {id}", "errorRetry": "Probeer het opnieuw", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Kosten", @@ -6216,7 +6220,8 @@ "kiro": "Gratis tier: 50 credits/maand (~25K–100K tokens). ⚠️ Kiro ToS verbiedt het gebruik van externe proxy/harness.", "codex": "Verbind OpenAI Codex met de bestaande OAuth-flow.", "qwen": "Verbind Qwen Code met de bestaande OAuth-flow.", - "github-models": "Maak een GitHub PAT aan met de scope 'models: read' op github.com/settings/tokens" + "github-models": "Maak een GitHub PAT aan met de scope 'models: read' op github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} accepteert provider-native model-ID's. Importeer uit /models of voeg aangepaste ID's toe voor routering.", "bedrockModelsDescription": "Amazon Bedrock-modellen zijn afgestemd op de AWS-regio. Importeer uit /models of voeg Bedrock-model-ID's toe die zijn ingeschakeld in de geselecteerde regio.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Beknopt CJK (文言)", "description": "Klassiek-Chinese ultra-beknopte stijl (alleen beschikbaar voor Chinees)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Wacht op afkoelen", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Gezondheid", "cliproxyapiPort": "Haven", "qdrantHost": "Host", - "qdrantCollection": "Verzameling" + "qdrantCollection": "Verzameling", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Opgeslagen.", "saveFailed": "Opslaan mislukt.", "enableAria": "Schakel de OmniGlyph-engine in", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Ingebedde services", @@ -9504,7 +9538,17 @@ "updatedShort": "Bijgewerkt", "lastRefreshed": "Laatst vernieuwd", "providerQuota": "Providerquota", - "providerQuotaHomeHint": "Live status over verbonden accounts" + "providerQuotaHomeHint": "Live status over verbonden accounts", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Wachten op toestemming", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 77a2074b7e..10b36bfe4a 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -3718,7 +3718,11 @@ "errorDescription": "Vi kunne ikke laste inn kombinasjonsdata akkurat nå. Sjekk tilkoblingen din og prøv igjen.", "errorId": "Feil-ID: {id}", "errorRetry": "Prøv igjen", - "comboLabel": "Kombinasjon" + "comboLabel": "Kombinasjon", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Kostnader", @@ -6216,7 +6220,8 @@ "kiro": "Gratisnivå: 50 kreditter/måned (~25K–100K tokens). ⚠️ Kiros brukervilkår forbyr bruk av tredjeparts proxy/harness.", "codex": "Koble til OpenAI Codex med den eksisterende OAuth-flyten.", "qwen": "Koble til Qwen Code med den eksisterende OAuth-flyten.", - "github-models": "Opprett et GitHub PAT med 'models: read'-omfang på github.com/settings/tokens" + "github-models": "Opprett et GitHub PAT med 'models: read'-omfang på github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} godtar leverandør-native modell-ID-er. Importer fra /models eller legg til egendefinerte ID-er for ruting.", "bedrockModelsDescription": "Amazon Bedrock-modeller er omfattet av AWS-regionen. Importer fra /models eller legg til berggrunnsmodell-IDer aktivert i den valgte regionen.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Kortfattet CJK (文言)", "description": "Klassisk-kinesisk ultrakortfattet stil (kun tilgjengelig for kinesisk)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Vent på nedkjøling", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Helse", "cliproxyapiPort": "Port", "qdrantHost": "Vert", - "qdrantCollection": "Samling" + "qdrantCollection": "Samling", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Lagret.", "saveFailed": "Kunne ikke lagre.", "enableAria": "Aktiver OmniGlyph-motoren", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Innebygde tjenester", @@ -9504,7 +9538,17 @@ "updatedShort": "Oppdatert", "lastRefreshed": "Sist oppdatert", "providerQuota": "Leverandørkvote", - "providerQuotaHomeHint": "Sanntidsstatus på tvers av tilkoblede kontoer" + "providerQuotaHomeHint": "Sanntidsstatus på tvers av tilkoblede kontoer", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Venter på autorisasjon", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index cfe0a6e006..09b239e750 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -3718,7 +3718,11 @@ "errorDescription": "Hindi namin ma-load ang combo data sa ngayon. Suriin ang iyong koneksyon at subukan muli.", "errorId": "Error ID: {id}", "errorRetry": "Subukan Muli", - "comboLabel": "Kumbinasyon" + "comboLabel": "Kumbinasyon", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Mga gastos", @@ -6216,7 +6220,8 @@ "kiro": "Libreng tier: 50 credits/buwan (~25K–100K tokens). ⚠️ Ipinagbabawal ng Kiro ToS ang paggamit ng third-party proxy/harness.", "codex": "Ikonekta ang OpenAI Codex gamit ang umiiral na OAuth flow.", "qwen": "Ikonekta ang Qwen Code gamit ang umiiral na OAuth flow.", - "github-models": "Gumawa ng GitHub PAT na may 'models: read' na scope sa github.com/settings/tokens" + "github-models": "Gumawa ng GitHub PAT na may 'models: read' na scope sa github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "Tumatanggap ang {provider} ng mga provider-native na model ID. Mag-import mula sa /models o magdagdag ng mga custom na ID para sa pagruruta.", "bedrockModelsDescription": "Ang mga modelo ng Amazon Bedrock ay saklaw ng rehiyon ng AWS. Mag-import mula sa /models o magdagdag ng mga Bedrock model ID na pinagana sa napiling rehiyon.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Maikling CJK (文言)", "description": "Klasikong Tsino na ultra-maikling estilo (magagamit lamang para sa Tsino)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Maghintay para sa Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Kalusugan", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Koleksyon" + "qdrantCollection": "Koleksyon", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Nai-save.", "saveFailed": "Hindi mai-save.", "enableAria": "I-enable ang OmniGlyph engine", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Mga Naka-embed na Serbisyo", @@ -9504,7 +9538,17 @@ "updatedShort": "Na-update", "lastRefreshed": "Huling na-refresh", "providerQuota": "Quota ng Provider", - "providerQuotaHomeHint": "Live na status sa lahat ng nakakonektang account" + "providerQuotaHomeHint": "Live na status sa lahat ng nakakonektang account", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Naghihintay ng Awtorisasyon", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 04590ea8ad..30a734c1dd 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -3718,7 +3718,11 @@ "errorDescription": "Nie mogliśmy teraz załadować danych combo. Sprawdź swoje połączenie i spróbuj ponownie.", "errorId": "Identyfikator błędu: {id}", "errorRetry": "Spróbuj ponownie", - "comboLabel": "Kombinacja" + "comboLabel": "Kombinacja", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Koszty", @@ -6216,7 +6220,8 @@ "kiro": "Darmowy plan: 50 kredytów/miesiąc (~25K–100K tokenów). ⚠️ ToS Kiro zabrania korzystania z zewnętrznych proxy/harness.", "codex": "Połącz OpenAI Codex za pomocą istniejącego przepływu OAuth.", "qwen": "Połącz Qwen Code za pomocą istniejącego przepływu OAuth.", - "github-models": "Utwórz token GitHub PAT z zakresem 'models: read' na github.com/settings/tokens" + "github-models": "Utwórz token GitHub PAT z zakresem 'models: read' na github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} akceptuje natywne ID models dla provider. Zaimportuj z /models lub dodaj własne ID do routing.", "bedrockModelsDescription": "Models Amazon Bedrock są ograniczone do regionu AWS. Zaimportuj z /models lub dodaj Bedrock ID models włączone w wybranym regionie.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Zwięzłe CJK (文言)", "description": "Klasyczny chiński styl ultra-zwięzły (dostępny tylko dla języka chińskiego)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Czekaj na cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Zdrowie", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Kolekcja" + "qdrantCollection": "Kolekcja", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "Silnik RTK", @@ -8624,7 +8637,28 @@ "saved": "Zapisano.", "saveFailed": "Nie można zapisać.", "enableAria": "Włącz silnik OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Usługi wbudowane", @@ -9504,7 +9538,17 @@ "updatedShort": "Zaktualizowano", "lastRefreshed": "Ostatnio odświeżono", "providerQuota": "Provider Quota", - "providerQuotaHomeHint": "Status na żywo na połączonych kontach" + "providerQuotaHomeHint": "Status na żywo na połączonych kontach", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Oczekiwanie na autoryzację", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index a4f8a5f3cf..00cdc772d9 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -3718,7 +3718,11 @@ "errorDescription": "Não conseguimos carregar os dados do combo no momento. Verifique sua conexão e tente novamente.", "errorId": "ID de Erro: {id}", "errorRetry": "Tente Novamente", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Custos", @@ -6216,7 +6220,8 @@ "kiro": "Nível gratuito: 50 créditos/mês (~25K–100K tokens). ⚠️ Os Termos de Serviço do Kiro proíbem o uso de proxy/harness de terceiros.", "codex": "Conecte o OpenAI Codex com o fluxo OAuth existente.", "qwen": "Conecte o Qwen Code com o fluxo OAuth existente.", - "github-models": "Crie um PAT do GitHub com o escopo 'models: read' em github.com/settings/tokens" + "github-models": "Crie um PAT do GitHub com o escopo 'models: read' em github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} aceita IDs de modelo nativos do provedor. Importe de /models ou adicione IDs personalizados para roteamento.", "bedrockModelsDescription": "Os modelos Amazon Bedrock têm escopo definido por região da AWS. Importe de /models ou adicione IDs de modelo Bedrock habilitados na região selecionada.", @@ -7210,6 +7215,7 @@ "configured": "configurado", "none": "Nenhum", "modelOverrideValuePlaceholder": "Valor numérico", + "modelOverrideReasoningEffortsPlaceholder": "Lista em inglês separada por vírgulas, ex. low, medium, high", "addKeyValue": "Adicionar valor da chave", "noModelOverrides": "Nenhuma substituição configurada para este modelo.", "modelOverrideLoadFailed": "Falha ao carregar substituições de modelo", @@ -7784,6 +7790,10 @@ "terse-cjk": { "label": "CJK conciso (文言)", "description": "Estilo ultra-conciso em chinês clássico (disponível apenas para chinês)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Aguarde o resfriamento", @@ -8216,7 +8226,11 @@ "cliproxyapiHealth": "Saúde", "cliproxyapiPort": "Porta", "qdrantHost": "Host", - "qdrantCollection": "Coleção" + "qdrantCollection": "Coleção", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "Motor RTK", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 9935bfe7ef..cb45bacae5 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -3718,7 +3718,11 @@ "errorDescription": "Não conseguimos carregar os dados do combo neste momento. Verifique a sua ligação e tente novamente.", "errorId": "ID de Erro: {id}", "errorRetry": "Tente Novamente", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Custos", @@ -6216,7 +6220,8 @@ "kiro": "Nível gratuito: 50 créditos/mês (~25K–100K tokens). ⚠️ Os ToS do Kiro proíbem a utilização de proxy/harness de terceiros.", "codex": "Ligar o OpenAI Codex com o fluxo OAuth existente.", "qwen": "Ligar o Qwen Code com o fluxo OAuth existente.", - "github-models": "Crie um PAT do GitHub com o âmbito 'models: read' em github.com/settings/tokens" + "github-models": "Crie um PAT do GitHub com o âmbito 'models: read' em github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} aceita IDs de modelo nativos do provedor. Importe de /models ou adicione IDs personalizados para roteamento.", "bedrockModelsDescription": "Os modelos Amazon Bedrock têm escopo definido por região da AWS. Importe de /models ou adicione IDs de modelo Bedrock habilitados na região selecionada.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK conciso (文言)", "description": "Estilo ultraconciso em chinês clássico (disponível apenas para chinês)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Aguarde o resfriamento", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Saúde", "cliproxyapiPort": "Porto", "qdrantHost": "Anfitrião", - "qdrantCollection": "Coleção" + "qdrantCollection": "Coleção", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "Motor RTK", @@ -8624,7 +8637,28 @@ "saved": "Guardado.", "saveFailed": "Não foi possível guardar.", "enableAria": "Ativar o motor OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Serviços Incorporados", @@ -9504,7 +9538,17 @@ "updatedShort": "Atualizado", "lastRefreshed": "Última atualização", "providerQuota": "Quota do fornecedor", - "providerQuotaHomeHint": "Estado em tempo real em todas as contas associadas" + "providerQuotaHomeHint": "Estado em tempo real em todas as contas associadas", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Aguardando autorização", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index cec84fa369..11f9dcdb85 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -3718,7 +3718,11 @@ "errorDescription": "Nu am putut încărca datele combo în acest moment. Verifică-ți conexiunea și încearcă din nou.", "errorId": "ID eroare: {id}", "errorRetry": "Încearcă din nou", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costuri", @@ -6216,7 +6220,8 @@ "kiro": "Nivel gratuit: 50 de credite/lună (~25K–100K tokenuri). ⚠️ Kiro ToS interzice utilizarea de proxy/harness terțe.", "codex": "Conectați OpenAI Codex cu fluxul OAuth existent.", "qwen": "Conectați Qwen Code cu fluxul OAuth existent.", - "github-models": "Creați un GitHub PAT cu scope-ul 'models: read' la github.com/settings/tokens" + "github-models": "Creați un GitHub PAT cu scope-ul 'models: read' la github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} acceptă ID-uri de model native ale furnizorului. Importați din /modele sau adăugați ID-uri personalizate pentru rutare.", "bedrockModelsDescription": "Modelele Amazon Bedrock sunt acoperite de regiunea AWS. Importați din /modele sau adăugați ID-uri de model Bedrock activate în regiunea selectată.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK concis (文言)", "description": "Stil ultra-concis în chineza clasică (disponibil doar pentru chineză)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Așteptați răcirea", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Sănătate", "cliproxyapiPort": "Port", "qdrantHost": "Gazdă", - "qdrantCollection": "Colecție" + "qdrantCollection": "Colecție", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Salvat.", "saveFailed": "Nu s-a putut salva.", "enableAria": "Activează motorul OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Servicii integrate", @@ -9504,7 +9538,17 @@ "updatedShort": "Actualizat", "lastRefreshed": "Ultima reîmprospătare", "providerQuota": "Cotă furnizor", - "providerQuotaHomeHint": "Stare în timp real pentru conturile conectate" + "providerQuotaHomeHint": "Stare în timp real pentru conturile conectate", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "În așteptarea autorizației", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index ba9a4e812e..108c4ce690 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -3718,7 +3718,11 @@ "errorDescription": "Мы не смогли загрузить данные комбо в данный момент. Проверьте ваше соединение и попробуйте снова.", "errorId": "Идентификатор ошибки: {id}", "errorRetry": "Попробуйте снова", - "comboLabel": "Комбо" + "comboLabel": "Комбо", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Затраты", @@ -6216,7 +6220,8 @@ "kiro": "Бесплатный тариф: 50 кредитов/месяц (~25K–100K токенов). ⚠️ Условия использования Kiro запрещают использование сторонних прокси/оболочек.", "codex": "Подключите OpenAI Codex с помощью существующего процесса OAuth.", "qwen": "Подключите Qwen Code с помощью существующего процесса OAuth.", - "github-models": "Создайте GitHub PAT с областью доступа 'models: read' на github.com/settings/tokens" + "github-models": "Создайте GitHub PAT с областью доступа 'models: read' на github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} принимает собственные идентификаторы моделей поставщика. Импортируйте из /models или добавляйте собственные идентификаторы для маршрутизации.", "bedrockModelsDescription": "Модели Amazon Bedrock охватываются регионом AWS. Импортируйте из /models или добавьте идентификаторы моделей Bedrock, включенные в выбранном регионе.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Краткий CJK (文言)", "description": "Ультракраткий классический китайский стиль (доступно только для китайского языка)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Подождите перезарядки", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Здоровье", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Коллекция" + "qdrantCollection": "Коллекция", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Сохранено.", "saveFailed": "Не удалось сохранить.", "enableAria": "Включить движок OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Встроенные службы", @@ -9504,7 +9538,17 @@ "updatedShort": "Обновлено", "lastRefreshed": "Последнее обновление", "providerQuota": "Квота Провайдера", - "providerQuotaHomeHint": "Живой статус по подключенным аккаунтам" + "providerQuotaHomeHint": "Живой статус по подключенным аккаунтам", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Ожидание авторизации", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index fbd89bef06..2b1f8c38c8 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -3718,7 +3718,11 @@ "errorDescription": "Momentálne sa nám nepodarilo načítať údaje kombinácie. Skontrolujte svoje pripojenie a skúste to znova.", "errorId": "Chyba ID: {id}", "errorRetry": "Skúste znova", - "comboLabel": "Kombinácia" + "comboLabel": "Kombinácia", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "náklady", @@ -6216,7 +6220,8 @@ "kiro": "Bezplatná úroveň: 50 kreditov/mesiac (~25k – 100k tokenov). ⚠️ Podmienky používania (ToS) Kiro zakazujú používanie proxy/harness tretích strán.", "codex": "Pripojte OpenAI Codex pomocou existujúceho toku OAuth.", "qwen": "Pripojte Qwen Code pomocou existujúceho toku OAuth.", - "github-models": "Vytvorte si GitHub PAT s rozsahom 'models: read' na adrese github.com/settings/tokens" + "github-models": "Vytvorte si GitHub PAT s rozsahom 'models: read' na adrese github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} akceptuje ID modelu natívneho poskytovateľa. Importujte z /models alebo pridajte vlastné ID pre smerovanie.", "bedrockModelsDescription": "Modely Amazon Bedrock sú vymedzené podľa regiónu AWS. Importovať z /models alebo pridať ID modelu Bedrock povolené vo vybranom regióne.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Stručné CJK (文言)", "description": "Klasický čínsky ultra stručný štýl (dostupný len pre čínštinu)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Počkajte na Cooldown", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Zdravie", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Zbierka" + "qdrantCollection": "Zbierka", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Uložené.", "saveFailed": "Nepodarilo sa uložiť.", "enableAria": "Povoliť engine OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Vstavané služby", @@ -9504,7 +9538,17 @@ "updatedShort": "Aktualizované", "lastRefreshed": "Naposledy obnovené", "providerQuota": "Kvóta poskytovateľa", - "providerQuotaHomeHint": "Aktuálny stav naprieč pripojenými účtami" + "providerQuotaHomeHint": "Aktuálny stav naprieč pripojenými účtami", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Čaká sa na autorizáciu", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 75b165386a..0aa1969eda 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -3718,7 +3718,11 @@ "errorDescription": "Vi kunde inte ladda combo-data just nu. Kontrollera din anslutning och försök igen.", "errorId": "Fel-ID: {id}", "errorRetry": "Försök igen", - "comboLabel": "Kombination" + "comboLabel": "Kombination", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Kostnader", @@ -6216,7 +6220,8 @@ "kiro": "Gratisnivå: 50 krediter/månad (~25K–100K tokens). ⚠️ Kiros användarvillkor förbjuder användning av tredjepartsproxy/harness.", "codex": "Anslut OpenAI Codex med det befintliga OAuth-flödet.", "qwen": "Anslut Qwen Code med det befintliga OAuth-flödet.", - "github-models": "Skapa en GitHub PAT med omfånget 'models: read' på github.com/settings/tokens" + "github-models": "Skapa en GitHub PAT med omfånget 'models: read' på github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} accepterar leverantörsbaserade modell-ID:n. Importera från /models eller lägg till anpassade ID:n för routing.", "bedrockModelsDescription": "Amazon Bedrock-modeller omfattas av AWS-regionen. Importera från /models eller lägg till Bedrock-modell-ID:n aktiverade i den valda regionen.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Kortfattad CJK (文言)", "description": "Klassisk kinesisk ultrakortfattad stil (endast tillgänglig för kinesiska)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Vänta på nedkylning", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Hälsa", "cliproxyapiPort": "Port", "qdrantHost": "Värd", - "qdrantCollection": "Samling" + "qdrantCollection": "Samling", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Sparat.", "saveFailed": "Kunde inte spara.", "enableAria": "Aktivera OmniGlyph-motorn", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Inbäddade tjänster", @@ -9504,7 +9538,17 @@ "updatedShort": "Uppdaterad", "lastRefreshed": "Senast uppdaterad", "providerQuota": "Leverantörskvot", - "providerQuotaHomeHint": "Livestatus för anslutna konton" + "providerQuotaHomeHint": "Livestatus för anslutna konton", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Väntar på auktorisering", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 15f8f0e290..77ae0f4373 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -3718,7 +3718,11 @@ "errorDescription": "Hatuwezi kupakia data ya combo kwa sasa. Angalia muunganisho wako na ujaribu tena.", "errorId": "Kosa ID: {id}", "errorRetry": "Jaribu Tena", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "Kiwango cha bure: mikopo 50/mwezi (~tokeni 25K–100K). ⚠️ Kiro ToS inakataza matumizi ya proksi/harness ya wahusika wengine.", "codex": "Unganisha OpenAI Codex na mtiririko uliopo wa OAuth.", "qwen": "Unganisha Qwen Code na mtiririko uliopo wa OAuth.", - "github-models": "Unda PAT ya GitHub yenye upeo wa 'models: read' kwenye github.com/settings/tokens" + "github-models": "Unda PAT ya GitHub yenye upeo wa 'models: read' kwenye github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} inakubali vitambulisho vya asili vya mtoa huduma. Ingiza kutoka /miundo au ongeza vitambulisho maalum vya kuelekeza.", "bedrockModelsDescription": "Mitindo ya Amazon Bedrock inatolewa na eneo la AWS. Ingiza kutoka /miundo au ongeza vitambulisho vya muundo wa Bedrock vilivyowezeshwa katika eneo lililochaguliwa.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK Fupi (文言)", "description": "Mtindo mfupi zaidi wa Kichina cha Kale (inapatikana kwa Kichina pekee)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Subiri hadi Kupunguza joto", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Afya", "cliproxyapiPort": "Bandari", "qdrantHost": "Mwenyeji", - "qdrantCollection": "Mkusanyiko" + "qdrantCollection": "Mkusanyiko", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Imehifadhiwa.", "saveFailed": "Imeshindwa kuhifadhi.", "enableAria": "Wezesha injini ya OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Huduma Zilizopachikwa", @@ -9504,7 +9538,17 @@ "updatedShort": "Imesasishwa", "lastRefreshed": "Ilihuishwa mara ya mwisho", "providerQuota": "Kiwango cha Mtoa Huduma", - "providerQuotaHomeHint": "Hali ya moja kwa moja kwenye akaunti zilizounganishwa" + "providerQuotaHomeHint": "Hali ya moja kwa moja kwenye akaunti zilizounganishwa", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index d9bb96f0f4..d944a2e6e4 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -3718,7 +3718,11 @@ "errorDescription": "நாங்கள் தற்போது கம்போ தரவுகளை ஏற்ற முடியவில்லை. உங்கள் இணைப்பை சரிபார்க்கவும் மற்றும் மீண்டும் முயற்சிக்கவும்.", "errorId": "பிழை அடையாளம்: {id}", "errorRetry": "மீண்டும் முயற்சி செய்", - "comboLabel": "கொம்போ" + "comboLabel": "கொம்போ", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "இலவச அடுக்கு: 50 கிரெடிட்கள்/மாதம் (~25K–100K டோக்கன்கள்). ⚠️ Kiro ToS மூன்றாம் தரப்பு proxy/harness பயன்பாட்டைத் தடைசெய்கிறது.", "codex": "தற்போதுள்ள OAuth செயல்முறையுடன் OpenAI Codex-ஐ இணைக்கவும்.", "qwen": "தற்போதுள்ள OAuth செயல்முறையுடன் Qwen Code-ஐ இணைக்கவும்.", - "github-models": "github.com/settings/tokens இல் 'models: read' வரம்புடன் ஒரு GitHub PAT ஐ உருவாக்கவும்" + "github-models": "github.com/settings/tokens இல் 'models: read' வரம்புடன் ஒரு GitHub PAT ஐ உருவாக்கவும்", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} வழங்குநரின் சொந்த மாதிரி ஐடிகளை ஏற்றுக்கொள்கிறது. /மாடல்களில் இருந்து இறக்குமதி செய்யவும் அல்லது ரூட்டிங் செய்ய தனிப்பயன் ஐடிகளைச் சேர்க்கவும்.", "bedrockModelsDescription": "அமேசான் பெட்ராக் மாதிரிகள் AWS பிராந்தியத்தால் ஸ்கோப் செய்யப்படுகின்றன. /மாடல்களில் இருந்து இறக்குமதி செய்யவும் அல்லது தேர்ந்தெடுக்கப்பட்ட பகுதியில் செயல்படுத்தப்பட்ட பெட்ராக் மாடல் ஐடிகளைச் சேர்க்கவும்.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "சுருக்கமான CJK (文言)", "description": "செம்மொழி-சீன மிகச் சுருக்கமான நடை (சீன மொழிக்கு மட்டுமே கிடைக்கும்)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "கூல்டவுனுக்காக காத்திருங்கள்", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "ஆரோக்கியம்", "cliproxyapiPort": "போர்ட்", "qdrantHost": "விருந்தினர்", - "qdrantCollection": "கலெக்ஷன்" + "qdrantCollection": "கலெக்ஷன்", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "சேமிக்கப்பட்டது.", "saveFailed": "சேமிக்க முடியவில்லை.", "enableAria": "OmniGlyph இயந்திரத்தை இயக்கு", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "உட்பொதிக்கப்பட்ட சேவைகள்", @@ -9504,7 +9538,17 @@ "updatedShort": "புதுப்பிக்கப்பட்டது", "lastRefreshed": "கடைசியாகப் புதுப்பிக்கப்பட்டது", "providerQuota": "வழங்குநர் ஒதுக்கீடு", - "providerQuotaHomeHint": "இணைக்கப்பட்ட கணக்குகளின் நேரலை நிலை" + "providerQuotaHomeHint": "இணைக்கப்பட்ட கணக்குகளின் நேரலை நிலை", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 65aa08d437..ac7538ee06 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -3718,7 +3718,11 @@ "errorDescription": "మేము ప్రస్తుతం కాంబో డేటాను లోడ్ చేయలేకపోయాము. మీ కనెక్షన్‌ను తనిఖీ చేసి మళ్లీ ప్రయత్నించండి.", "errorId": "లోపం ID: {id}", "errorRetry": "మరలా ప్రయత్నించండి", - "comboLabel": "కాంబో" + "comboLabel": "కాంబో", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "ఉచిత శ్రేణి: 50 క్రెడిట్‌లు/నెల (~25K–100K టోకెన్‌లు). ⚠️ Kiro ToS థర్డ్-పార్టీ ప్రాక్సీ/హార్నెస్ వినియోగాన్ని నిషేధిస్తుంది.", "codex": "ప్రస్తుత OAuth flowతో OpenAI Codexని కనెక్ట్ చేయండి.", "qwen": "ప్రస్తుత OAuth flowతో Qwen Codeని కనెక్ట్ చేయండి.", - "github-models": "github.com/settings/tokens వద్ద 'models: read' స్కోప్‌తో GitHub PATని సృష్టించండి" + "github-models": "github.com/settings/tokens వద్ద 'models: read' స్కోప్‌తో GitHub PATని సృష్టించండి", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} ప్రొవైడర్-స్థానిక మోడల్ IDలను అంగీకరిస్తుంది. /మోడల్స్ నుండి దిగుమతి చేయండి లేదా రూటింగ్ కోసం అనుకూల IDలను జోడించండి.", "bedrockModelsDescription": "అమెజాన్ బెడ్‌రాక్ మోడల్‌లు AWS ప్రాంతం ద్వారా స్కోప్ చేయబడ్డాయి. /మోడల్స్ నుండి దిగుమతి చేయండి లేదా ఎంచుకున్న ప్రాంతంలో ప్రారంభించబడిన బెడ్‌రాక్ మోడల్ IDలను జోడించండి.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "సంక్షిప్త CJK (文言)", "description": "క్లాసికల్-చైనీస్ అల్ట్రా-సంక్షిప్త శైలి (చైనీస్ కోసం మాత్రమే అందుబాటులో ఉంది)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "కూల్‌డౌన్ కోసం వేచి ఉండండి", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "ఆరోగ్యం", "cliproxyapiPort": "పోర్ట్", "qdrantHost": "హోస్ట్", - "qdrantCollection": "సేకరణ" + "qdrantCollection": "సేకరణ", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "సేవ్ చేయబడింది.", "saveFailed": "సేవ్ చేయడం సాధ్యపడలేదు.", "enableAria": "OmniGlyph ఇంజిన్‌ను ఎనేబుల్ చేయండి", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "ఎంబెడెడ్ సేవలు", @@ -9504,7 +9538,17 @@ "updatedShort": "అప్‌డేట్ చేయబడింది", "lastRefreshed": "చివరిగా రిఫ్రెష్ చేయబడింది", "providerQuota": "ప్రొవైడర్ కోటా", - "providerQuotaHomeHint": "కనెక్ట్ చేయబడిన ఖాతాల ప్రత్యక్ష స్థితి" + "providerQuotaHomeHint": "కనెక్ట్ చేయబడిన ఖాతాల ప్రత్యక్ష స్థితి", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index ed1e5d4063..c7bc2b9524 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -3718,7 +3718,11 @@ "errorDescription": "ไม่สามารถโหลดข้อมูลคอมโบได้ในขณะนี้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองอีกครั้ง.", "errorId": "รหัสข้อผิดพลาด: {id}", "errorRetry": "ลองอีกครั้ง", - "comboLabel": "คอมโบ" + "comboLabel": "คอมโบ", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "ค่าใช้จ่าย", @@ -6216,7 +6220,8 @@ "kiro": "ระดับฟรี: 50 เครดิต/เดือน (~25K–100K โทเค็น) ⚠️ ToS ของ Kiro ห้ามใช้พร็อกซี/harness ของบุคคลที่สาม", "codex": "เชื่อมต่อ OpenAI Codex ด้วยโฟลว์ OAuth ที่มีอยู่", "qwen": "เชื่อมต่อ Qwen Code ด้วยโฟลว์ OAuth ที่มีอยู่", - "github-models": "สร้าง GitHub PAT ที่มีขอบเขต 'models: read' ที่ github.com/settings/tokens" + "github-models": "สร้าง GitHub PAT ที่มีขอบเขต 'models: read' ที่ github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} ยอมรับ ID โมเดลดั้งเดิมของผู้ให้บริการ นำเข้าจาก /models หรือเพิ่ม ID ที่กำหนดเองสำหรับการกำหนดเส้นทาง", "bedrockModelsDescription": "โมเดล Amazon Bedrock มีการกำหนดขอบเขตตามภูมิภาค AWS นำเข้าจาก /models หรือเพิ่มรหัสรุ่น Bedrock ที่เปิดใช้งานในภูมิภาคที่เลือก", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "CJK แบบกระชับ (文言)", "description": "สไตล์ภาษาจีนคลาสสิกแบบกระชับอย่างยิ่ง (ใช้ได้เฉพาะภาษาจีนเท่านั้น)" + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "รอคูลดาวน์", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "สุขภาพ", "cliproxyapiPort": "พอร์ต", "qdrantHost": "โฮสต์", - "qdrantCollection": "การรวบรวม" + "qdrantCollection": "การรวบรวม", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "บันทึกแล้ว", "saveFailed": "ไม่สามารถบันทึกได้", "enableAria": "เปิดใช้งานเอนจิน OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "บริการแบบฝัง", @@ -9504,7 +9538,17 @@ "updatedShort": "อัปเดตแล้ว", "lastRefreshed": "รีเฟรชล่าสุดเมื่อ", "providerQuota": "โควตาผู้ให้บริการ", - "providerQuotaHomeHint": "สถานะแบบเรียลไทม์ของบัญชีที่เชื่อมต่อทั้งหมด" + "providerQuotaHomeHint": "สถานะแบบเรียลไทม์ของบัญชีที่เชื่อมต่อทั้งหมด", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "รอการอนุญาต", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 5ef5816345..96ff186627 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -3718,7 +3718,11 @@ "errorDescription": "Şu anda kombinasyon verilerini yükleyemedik. Bağlantınızı kontrol edin ve tekrar deneyin.", "errorId": "Hata Kimliği: {id}", "errorRetry": "Tekrar Dene", - "comboLabel": "Kombinasyon" + "comboLabel": "Kombinasyon", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Maliyetler", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Mevcut OAuth akışı ile OpenAI Codex'i bağlayın.", "qwen": "Mevcut OAuth akışı ile Qwen Code'u bağlayın.", - "github-models": "github.com/settings/tokens adresinde 'models: read' kapsamına sahip bir GitHub PAT oluşturun" + "github-models": "github.com/settings/tokens adresinde 'models: read' kapsamına sahip bir GitHub PAT oluşturun", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} sağlayıcıya özgü model kimliklerini kabul eder. /models'den içe aktarın veya yönlendirme için özel kimlikler ekleyin.", "bedrockModelsDescription": "Amazon Bedrock modelleri AWS bölgesi kapsamındadır. /models'den içe aktarın veya seçilen bölgede etkin olan Bedrock model kimliklerini ekleyin.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Kısa ve öz CJK (文言)", "description": "Klasik Çince ultra kısa ve öz stil (yalnızca Çince için kullanılabilir)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Bekleme Süresini Bekleyin", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Sağlık", "cliproxyapiPort": "Port", "qdrantHost": "Ana Bilgisayar", - "qdrantCollection": "Koleksiyon" + "qdrantCollection": "Koleksiyon", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "Kaydedildi.", "saveFailed": "Kaydedilemedi.", "enableAria": "OmniGlyph motorunu etkinleştir", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Gömülü Servisler", @@ -9504,7 +9538,17 @@ "updatedShort": "Güncellendi", "lastRefreshed": "Son yenileme", "providerQuota": "Sağlayıcı Kotası", - "providerQuotaHomeHint": "Bağlı hesaplar genelinde canlı durum" + "providerQuotaHomeHint": "Bağlı hesaplar genelinde canlı durum", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Yetkilendirme bekleniyor", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 9fe5efbd52..2194b88f3d 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -3718,7 +3718,11 @@ "errorDescription": "Ми не змогли завантажити дані комбо прямо зараз. Перевірте своє з'єднання та спробуйте ще раз.", "errorId": "Ідентифікатор помилки: {id}", "errorRetry": "Спробуйте ще раз", - "comboLabel": "Комбо" + "comboLabel": "Комбо", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Витрати", @@ -6216,7 +6220,8 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "Створіть GitHub PAT з областю видимості 'models: read' на github.com/settings/tokens" + "github-models": "Створіть GitHub PAT з областю видимості 'models: read' на github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} приймає власні ідентифікатори моделі постачальника. Імпортуйте з /models або додайте власні ідентифікатори для маршрутизації.", "bedrockModelsDescription": "Моделі Amazon Bedrock залежать від регіону AWS. Імпортуйте з /models або додайте ідентифікатори моделі Bedrock, активовані у вибраному регіоні.", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "Стисла CJK (文言)", "description": "Класичний китайський ультрастислий стиль (доступно тільки для китайської)." + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Дочекайтеся перезарядки", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "Здоров'я", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Колекція" + "qdrantCollection": "Колекція", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "Двигун RTK", @@ -8624,7 +8637,28 @@ "saved": "Збережено.", "saveFailed": "Не вдалося зберегти.", "enableAria": "Увімкнути рушій OmniGlyph", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "Вбудовані служби", @@ -9504,7 +9538,17 @@ "updatedShort": "Оновлено", "lastRefreshed": "Останнє оновлення", "providerQuota": "Квота провайдера", - "providerQuotaHomeHint": "Стан у реальному часі за підключеними акаунтами" + "providerQuotaHomeHint": "Стан у реальному часі за підключеними акаунтами", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Очікування авторизації", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 22deb83299..2fab849e16 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -3718,7 +3718,11 @@ "errorDescription": "ہم اس وقت کومبو ڈیٹا لوڈ نہیں کر سکے۔ اپنی کنکشن چیک کریں اور دوبارہ کوشش کریں۔", "errorId": "خرابی کی شناخت: {id}", "errorRetry": "پھر کوشش کریں", - "comboLabel": "کمبو" + "comboLabel": "کمبو", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Costs", @@ -6216,7 +6220,8 @@ "kiro": "مفت پلان: 50 کریڈٹس/مہینہ (~25K–100K ٹوکنز)۔ ⚠️ Kiro ToS فریقِ ثالث کے پراکسی/ہارنس کے استعمال سے منع کرتا ہے۔", "codex": "OpenAI Codex کو موجودہ OAuth فلو سے منسلک کریں۔", "qwen": "Qwen Code کو موجودہ OAuth فلو سے منسلک کریں۔", - "github-models": "github.com/settings/tokens پر 'models: read' اسکوپ کے ساتھ ایک GitHub PAT بنائیں" + "github-models": "github.com/settings/tokens پر 'models: read' اسکوپ کے ساتھ ایک GitHub PAT بنائیں", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} فراہم کنندہ کے مقامی ماڈل IDs کو قبول کرتا ہے۔ /ماڈلز سے درآمد کریں یا روٹنگ کے لیے حسب ضرورت IDs شامل کریں۔", "bedrockModelsDescription": "ایمیزون بیڈرک ماڈلز کا دائرہ AWS ریجن کے ذریعہ کیا گیا ہے۔ /ماڈلز سے درآمد کریں یا منتخب علاقے میں فعال کردہ Bedrock ماڈل IDs شامل کریں۔", @@ -7784,6 +7789,10 @@ "terse-cjk": { "label": "مختصر CJK (文言)", "description": "کلاسیکی چینی انتہائی مختصر انداز (صرف چینی زبان کے لیے دستیاب ہے)۔" + }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "کولڈاؤن کا انتظار کریں۔", @@ -8216,7 +8225,11 @@ "cliproxyapiHealth": "صحت", "cliproxyapiPort": "پورٹ", "qdrantHost": "میزبان", - "qdrantCollection": "اجتماع" + "qdrantCollection": "اجتماع", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." }, "contextRtk": { "title": "RTK Engine", @@ -8624,7 +8637,28 @@ "saved": "محفوظ ہو گیا۔", "saveFailed": "محفوظ نہیں ہو سکا۔", "enableAria": "OmniGlyph انجن فعال کریں", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "ایمبیڈڈ سروسز", @@ -9504,7 +9538,17 @@ "updatedShort": "اپ ڈیٹ شدہ", "lastRefreshed": "آخری بار ریفریش کیا گیا", "providerQuota": "فراہم کنندہ کا کوٹہ", - "providerQuotaHomeHint": "منسلک اکاؤنٹس میں لائیو اسٹیٹس" + "providerQuotaHomeHint": "منسلک اکاؤنٹس میں لائیو اسٹیٹس", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index eb1cb41b6e..3953260e5c 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -3718,7 +3718,11 @@ "errorDescription": "Hiện không thể tải dữ liệu combo. Hãy kiểm tra kết nối rồi thử lại.", "errorId": "ID lỗi: {id}", "errorRetry": "Thử lại", - "comboLabel": "Combo" + "comboLabel": "Combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "Chi phí", @@ -6216,7 +6220,8 @@ "kiro": "Gói miễn phí: 50 tín dụng/tháng (khoảng 25–100 nghìn token). ⚠️ Điều khoản Kiro cấm sử dụng proxy/harness của bên thứ ba.", "codex": "Kết nối OpenAI Codex bằng luồng OAuth hiện có.", "qwen": "Kết nối Qwen Code bằng luồng OAuth hiện có.", - "github-models": "Tạo GitHub PAT với phạm vi 'models: read' tại github.com/settings/tokens" + "github-models": "Tạo GitHub PAT với phạm vi 'models: read' tại github.com/settings/tokens", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} chấp nhận ID mô hình gốc của nhà cung cấp. Nhập từ /models hoặc thêm ID tùy chỉnh để định tuyến.", "bedrockModelsDescription": "Các mô hình Amazon Bedrock được giới hạn theo vùng AWS. Nhập từ /models hoặc thêm ID mô hình Bedrock được bật trong vùng đã chọn.", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 31d1a87e10..1c58b63e59 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -3718,7 +3718,11 @@ "errorDescription": "我们现在无法加载组合数据。请检查您的连接并重试。", "errorId": "错误 ID: {id}", "errorRetry": "再试一次", - "comboLabel": "组合" + "comboLabel": "组合", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "成本", @@ -6227,7 +6231,8 @@ "kiro": "免费层:50 积分/月(约 25K–100K 令牌)。⚠️ Kiro 服务条款禁止使用第三方代理/测试框架。", "codex": "使用现有的 OAuth 流程连接 OpenAI Codex。", "qwen": "使用现有的 OAuth 流程连接 Qwen Code。", - "github-models": "在 github.com/settings/tokens 创建具有 'models: read' 作用域的 GitHub PAT" + "github-models": "在 github.com/settings/tokens 创建具有 'models: read' 作用域的 GitHub PAT", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} 接受供应商本机模型 ID。从 /models 导入或添加用于路由的自定义 ID。", "bedrockModelsDescription": "Amazon Bedrock 模型的范围按 AWS 区域划分。从 /models 导入或添加在所选区域中启用的基岩模型 ID。", @@ -8632,7 +8637,28 @@ "saved": "已保存。", "saveFailed": "无法保存。", "enableAria": "启用 OmniGlyph 引擎", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "嵌入式服务", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 6bc5f1954d..ddec1a0f6e 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -3718,7 +3718,11 @@ "errorDescription": "目前無法加載組合數據。請檢查您的連接並重試。", "errorId": "錯誤 ID: {id}", "errorRetry": "再試一次", - "comboLabel": "組合" + "comboLabel": "組合", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." }, "costs": { "title": "成本", @@ -6216,7 +6220,8 @@ "kiro": "免費方案:每月 50 額度(約 2.5 萬至 10 萬 tokens)。⚠️ Kiro 服務條款禁止第三方代理/轉接使用。", "codex": "使用現有的 OAuth 流程連線 OpenAI Codex。", "qwen": "使用現有的 OAuth 流程連線 Qwen Code。", - "github-models": "在 github.com/settings/tokens 建立具有 'models: read' 範圍的 GitHub PAT" + "github-models": "在 github.com/settings/tokens 建立具有 'models: read' 範圍的 GitHub PAT", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." }, "passthroughModelsDescription": "{provider} 接受提供者本機模型 ID。從 /models 匯入或新增用於路由的自定義 ID。", "bedrockModelsDescription": "Amazon Bedrock 模型的範圍按 AWS 區域劃分。從 /models 匯入或新增在所選區域中啟用的基岩模型 ID。", @@ -8632,7 +8637,28 @@ "saved": "已儲存。", "saveFailed": "無法儲存。", "enableAria": "啟用 OmniGlyph 引擎", - "title": "OmniGlyph" + "title": "OmniGlyph", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile" }, "embeddedServices": { "title": "內嵌服務", diff --git a/src/lib/modelMetadataRegistry.ts b/src/lib/modelMetadataRegistry.ts index c5c0bbbb7c..5e477084ea 100644 --- a/src/lib/modelMetadataRegistry.ts +++ b/src/lib/modelMetadataRegistry.ts @@ -29,7 +29,6 @@ import { CANONICAL_EFFORT_VALUES, extendCodexGpt56EffortValues, } from "@/shared/reasoning/effortStandardization"; -import type { ModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot"; const MODEL_METADATA_SCHEMA_VERSION = "model-metadata-v1"; diff --git a/stryker.conf.json b/stryker.conf.json index 08c35843f5..59b52acdac 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -64,6 +64,7 @@ "tests/unit/adobe-firefly.test.ts", "tests/unit/agentrouter-error-rules.test.ts", "tests/unit/agentrouter-lock-scope-10334.test.ts", + "tests/unit/aihorde-optional-api-key.test.ts", "tests/unit/alibaba-free-tier-exhaustion.test.ts", "tests/unit/anthropic-thinking-signature-recovery.test.ts", "tests/unit/antigravity-429-quota-tdd.test.ts", @@ -150,8 +151,10 @@ "tests/unit/codex-failover.test.ts", "tests/unit/codex-quota-selection-hydration.test.ts", "tests/unit/codex-responses-to-chat-9161.test.ts", + "tests/unit/codex-same-account-transport-retry-9708.test.ts", "tests/unit/codex-session-affinity-reset-aware-5903.test.ts", "tests/unit/codex-stream-false.test.ts", + "tests/unit/codex-turn-state.test.ts", "tests/unit/collect-metrics-module-coverage.test.ts", "tests/unit/combo-499-abort.test.ts", "tests/unit/combo-account-allowlist-3266.test.ts", @@ -294,6 +297,9 @@ "tests/unit/quota-pool-log-route.test.ts", "tests/unit/quota-streaming-consumption-usd.test.ts", "tests/unit/qwen-web-content-array-serialization.test.ts", + "tests/unit/probe-gate-autodisable.test.ts", + "tests/unit/probe-production-path.test.ts", + "tests/unit/probe-testall-isolation.test.ts", "tests/unit/rate-limit-enhanced.test.ts", "tests/unit/rate-limit-execution-timeout-message-4165.test.ts", "tests/unit/rate-limit-local-capacity-classification.test.ts", @@ -307,6 +313,7 @@ "tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts", "tests/unit/resilience-connections.test.ts", "tests/unit/responses-handler.test.ts", + "tests/unit/responses-passthrough-openai-compatible.test.ts", "tests/unit/rotation-config-omniroute.test.ts", "tests/unit/route-explainability.test.ts", "tests/unit/route-guard-acp-agents-local-only.test.ts", @@ -335,6 +342,7 @@ "tests/unit/skip-provider-breaker-consumer-2743.test.ts", "tests/unit/sse-auth-antigravity-credits.test.ts", "tests/unit/sse-auth-codex-account-pool.test.ts", + "tests/unit/sse-auth-exclusive-leases.test.ts", "tests/unit/sse-auth-resource-404.test.ts", "tests/unit/sse-auth.test.ts", "tests/unit/db/stats-dbstat-optional.test.ts", diff --git a/tests/unit/check-env-doc-sync.test.ts b/tests/unit/check-env-doc-sync.test.ts index 2db6a62c87..3975e58361 100644 --- a/tests/unit/check-env-doc-sync.test.ts +++ b/tests/unit/check-env-doc-sync.test.ts @@ -179,6 +179,32 @@ test("runEnvDocSync: ignore set skips a code-referenced var", () => { assert.equal(result.ok, true); }); +test("runEnvDocSync: shipped allowlist ignores ad-hoc BOT_TOKEN and BOT_URL", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const codeVars = new Set(["JWT_SECRET", "BOT_TOKEN", "BOT_URL"]); + + const unignored = runEnvDocSync({ + envExampleText, + envDocText, + codeVars, + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(unignored.ok, false); + assert.deepEqual(unignored.problems.codeMissingEnv, ["BOT_TOKEN", "BOT_URL"]); + + // Omit `ignore` so the checker uses IGNORE_FROM_CODE from check-env-doc-sync.mjs. + const shipped = runEnvDocSync({ + envExampleText, + envDocText, + codeVars, + }); + assert.equal(shipped.ok, true); + assert.deepEqual(shipped.problems.codeMissingEnv, []); +}); + test("repository contract is in sync (live data)", () => { // Uses the real .env.example, docs/ENVIRONMENT.md, and the bundled // allowlists. This is the same check that runs in pre-commit / CI. diff --git a/tests/unit/combo-context-overflow-compression-probe.test.ts b/tests/unit/combo-context-overflow-compression-probe.test.ts index f482cba352..fd602564c9 100644 --- a/tests/unit/combo-context-overflow-compression-probe.test.ts +++ b/tests/unit/combo-context-overflow-compression-probe.test.ts @@ -32,9 +32,7 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const { saveModelsDevCapabilities, clearModelsDevCapabilities } = await import("../../src/lib/modelsDevSync.ts"); -const { getKnownContextOverflow, handleComboChat } = await import( - "../../open-sse/services/combo.ts" -); +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); const { updateCompressionSettings } = await import("../../src/lib/db/compression.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); @@ -74,20 +72,6 @@ function capabilityEntry(limitContext: number | null) { }; } -function target(modelStr: string) { - return { - kind: "model" as const, - stepId: modelStr, - executionKey: modelStr, - modelStr, - provider: modelStr.includes("/") ? modelStr.split("/")[0] : modelStr, - providerId: null, - connectionId: null, - weight: 1, - label: null, - }; -} - // A generic Responses-API body whose estimate lands near `tokens` tokens (4 chars/token). // Uses `input:` (not `messages:`) to mirror the OpenCode/Codex Responses surface. function bigResponsesBody(tokens: number) { @@ -96,32 +80,6 @@ function bigResponsesBody(tokens: number) { const noopLog = { info() {}, warn() {}, error() {}, debug() {} }; -test("#10225 getKnownContextOverflow defers the hard overflow when compression is available", () => { - saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); - const body = bigResponsesBody(275_000); - - // Compression enabled + target can compress -> defer (null). - assert.equal( - getKnownContextOverflow([target("codex/gpt-5.6-terra")], body, { - deferContextOverflowWhenCompressible: true, - }), - null, - "compressible request must defer so chatCore compression can run (#10225)" - ); - - // Compression disabled -> the existing hard overflow is preserved (never lose #7177). - const hard = getKnownContextOverflow([target("codex/gpt-5.6-terra")], body); - assert.ok(hard); - assert.ok(hard.requiredContextTokens > hard.maxKnownContextTokens); - - // Compression enabled but EVERY target is excluded from compression -> keep the hard gate. - const excluded = getKnownContextOverflow([target("codex/gpt-5.6-terra")], body, { - deferContextOverflowWhenCompressible: true, - compressionExclusions: ["gpt-5.6-terra"], - }); - assert.ok(excluded, "fully-excluded targets must retain the hard preflight"); -}); - test("#10225 combo does not early-400 a compressible over-limit request when deferral is on", async () => { saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); let dispatches = 0; @@ -147,7 +105,7 @@ test("#10225 combo does not early-400 a compressible over-limit request when def assert.equal(dispatches, 1, "must dispatch so chatCore compaction runs first"); }); -test("#10225 combo keeps the fast 400 when compression is disabled", async () => { +test("#10225 combo does not hard-400 an over-limit request when compression is disabled", async () => { saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); let dispatches = 0; @@ -168,10 +126,10 @@ test("#10225 combo keeps the fast 400 when compression is disabled", async () => log: noopLog, }); - assert.equal(response.status, 400); - assert.equal(dispatches, 0, "#7177 anti-exhaustion guard must survive when compression is off"); - const body = await response.json(); - assert.equal(body.error.code, "context_length_exceeded"); + // #10162: gateway chars/4 estimates are advisory. Compression off is not a + // pre-dispatch 400; chatCore / upstream remain the context gate. + assert.notEqual(response.status, 400, "advisory estimates must not hard-400 before dispatch (#10162)"); + assert.equal(dispatches, 1, "must dispatch when local overflow estimates are advisory"); }); // #10501-sweep #10503 — the deferral above is NOT target-aware by default: it only @@ -193,47 +151,7 @@ test("#10225 combo keeps the fast 400 when compression is disabled", async () => // combo member over `/v1/responses` in openai-responses format still hits chatCore's // compression bypass — exactly the gap `sourceFormat`/`endpointPath` (not the looser // `clientManagedResponsesContext` flag) now closes. -test("#10503 getKnownContextOverflow REFUSES to defer when the only target is native Codex Responses passthrough", () => { - saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); - const body = bigResponsesBody(275_000); - - const overflow = getKnownContextOverflow([target("codex/gpt-5.6-terra")], body, { - deferContextOverflowWhenCompressible: true, - sourceFormat: "openai-responses", - endpointPath: "/v1/responses", - }); - - assert.ok( - overflow, - "a native-codex-passthrough target must never be treated as compressible — the " + - "hard preflight must stay active (chatCore disables compression for it entirely)" - ); -}); - -test("#10503 getKnownContextOverflow still defers when a genuinely compressible sibling target is present", () => { - saveModelsDevCapabilities({ - codex: { "gpt-5.6-terra": capabilityEntry(272_000) }, - openai: { "gpt-5.6-terra": capabilityEntry(272_000) }, - }); - const body = bigResponsesBody(275_000); - - // A heterogeneous pool where at least ONE target (openai) genuinely runs - // compression must still defer — deferral is a per-request decision, and other - // targets in the pool are unaffected by the codex-specific compression bypass. - const overflow = getKnownContextOverflow( - [target("codex/gpt-5.6-terra"), target("openai/gpt-5.6-terra")], - body, - { - deferContextOverflowWhenCompressible: true, - sourceFormat: "openai-responses", - endpointPath: "/v1/responses", - } - ); - - assert.equal(overflow, null, "a genuinely compressible sibling target must still defer"); -}); - -test("#10503 handleComboChat: native-codex-passthrough pool fails FAST locally, zero upstream dispatches", async () => { +test("#10503 handleComboChat: native-codex-passthrough pool still dispatches oversized requests", async () => { saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); let dispatches = 0; @@ -255,14 +173,14 @@ test("#10503 handleComboChat: native-codex-passthrough pool fails FAST locally, log: noopLog, }); - assert.equal( + // #10162 removed the chars/4 pre-dispatch 400. Native Codex passthrough still + // reaches the target; chatCore / upstream enforce the real context limit. + assert.notEqual( response.status, 400, - "must fail fast locally instead of dispatching an oversized, uncompressible request" + "must not fail fast locally on an advisory overflow estimate (#10162)" ); - assert.equal(dispatches, 0, "no wasted upstream call for a target that can never compress"); - const responseBody = await response.json(); - assert.equal(responseBody.error.code, "context_length_exceeded"); + assert.equal(dispatches, 1, "advisory estimates must not skip upstream dispatch"); }); // #10503 item 2 — drive the REAL chatCore compression pipeline end-to-end (not just the diff --git a/tests/unit/db-driver-bundling-externals.test.ts b/tests/unit/db-driver-bundling-externals.test.ts index b1d920dd0e..c7dcc0d06c 100644 --- a/tests/unit/db-driver-bundling-externals.test.ts +++ b/tests/unit/db-driver-bundling-externals.test.ts @@ -36,7 +36,10 @@ test("sync driver cascade requires each SQLite module by literal specifier", () // The production loader must be the literal-specifier wrapper, never `_require` // itself — passing `_require` through the `load` parameter is exactly what makes // webpack substitute its missing-module stub. - assert.match(driverFactory, /^const openSyncDriver = createSyncDriverFactory\(\w+\);$/m); + assert.match( + driverFactory, + /^const openSyncDriver = createSyncDriverFactory\(\w+(?:,[\s\S]*?)?\);$/m + ); assert.match(driverFactory, /^export function tryOpenSync\($/m); assert.doesNotMatch(driverFactory, /createSyncDriverFactory\(\s*_require\s*\)/); diff --git a/tests/unit/hard-session-lease-bypass-inventory.test.ts b/tests/unit/hard-session-lease-bypass-inventory.test.ts index 428b75bdcf..24507e00c8 100644 --- a/tests/unit/hard-session-lease-bypass-inventory.test.ts +++ b/tests/unit/hard-session-lease-bypass-inventory.test.ts @@ -15,12 +15,16 @@ const EXPECTED: Record> = { credential: { "open-sse/handlers/chatCore.ts": 1, "open-sse/services/imageCombo.ts": 1, + "open-sse/services/speechCombo.ts": 1, + "open-sse/services/videoCombo.ts": 2, "src/app/api/compression/compare/verify/route.ts": 1, "src/app/api/internal/codex-responses-ws/route.ts": 1, "src/app/api/search/providers/route.ts": 3, + "src/app/api/v1/_shared/videoModelResolution.ts": 1, "src/app/api/v1/audio/speech/route.ts": 1, - "src/app/api/v1/audio/transcriptions/route.ts": 1, + "src/app/api/v1/audio/transcriptions/route.ts": 2, "src/app/api/v1/audio/translations/route.ts": 1, + "src/app/api/v1/classify/route.ts": 1, "src/app/api/v1/images/edits/route.ts": 5, "src/app/api/v1/images/generations/route.ts": 3, "src/app/api/v1/images/upscale/route.ts": 1, @@ -32,8 +36,9 @@ const EXPECTED: Record> = { "src/app/api/v1/providers/[provider]/images/generations/route.ts": 1, "src/app/api/v1/rerank/route.ts": 2, "src/app/api/v1/search/route.ts": 2, + "src/app/api/v1/segment/route.ts": 1, "src/app/api/v1/session-leases/route.ts": 1, - "src/app/api/v1/videos/generations/route.ts": 3, + "src/app/api/v1/videos/generations/route.ts": 2, "src/app/api/v1/web/fetch/route.ts": 1, "src/lib/embeddings/service.ts": 2, "src/lib/memory/embedding/index.ts": 1, @@ -49,6 +54,7 @@ const EXPECTED: Record> = { "open-sse/handlers/chatCore/cliproxyapiCredentials.ts": 1, "open-sse/handlers/imageGeneration.ts": 1, "open-sse/handlers/imageGeneration/providers/chatgptWeb.ts": 1, + "open-sse/handlers/imageGeneration/providers/geminiWeb.ts": 1, "open-sse/handlers/videoGeneration.ts": 1, "open-sse/services/compression/eval/executorModelClient.ts": 1, "src/lib/compression/judgeModelClient.ts": 1, @@ -149,6 +155,7 @@ const CLASSIFICATION: Record> = { "open-sse/handlers/chatCore/cliproxyapiCredentials.ts": "A", "open-sse/handlers/imageGeneration.ts": "B", "open-sse/handlers/imageGeneration/providers/chatgptWeb.ts": "B", + "open-sse/handlers/imageGeneration/providers/geminiWeb.ts": "B", "open-sse/handlers/videoGeneration.ts": "B", "open-sse/services/compression/eval/executorModelClient.ts": "B", "src/lib/compression/judgeModelClient.ts": "B", diff --git a/tests/unit/sse-heartbeat.test.ts b/tests/unit/sse-heartbeat.test.ts index 57ecbd9450..94f39cfebc 100644 --- a/tests/unit/sse-heartbeat.test.ts +++ b/tests/unit/sse-heartbeat.test.ts @@ -1,6 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; +// #10524 default is comment-heartbeats off. This file asserts heartbeat payloads, +// so opt in for the suite (restored in process teardown is unnecessary: node:test +// worker is dedicated). +process.env.OMNIROUTE_SSE_COMMENTS = "on"; + const { createSseHeartbeatTransform } = await import("../../open-sse/utils/sseHeartbeat.ts"); function withFakeIntervals(fn) { From 8c4a219746583842c82e8a5de55276fcf81ff4db Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 11:55:04 -0300 Subject: [PATCH 069/135] Revert "chore(ci): ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync (#10828)" This reverts commit 7288fa0dd7846b1f01ee85c7f120a1d217039fc0. --- AGENTS.md | 2 +- README.md | 8 +- .../maintenance/env-doc-sync-adhoc-bot.md | 1 - .../release-v3850-basereds-drain-20260820.md | 1 - config/quality/quality-baseline.json | 3 +- docs/diagrams/promise-pillars.svg | 2 +- docs/i18n/ar/llm.txt | 12 +- docs/i18n/az/llm.txt | 12 +- docs/i18n/bg/llm.txt | 12 +- docs/i18n/bn/llm.txt | 12 +- docs/i18n/cs/llm.txt | 12 +- docs/i18n/da/llm.txt | 12 +- docs/i18n/de/llm.txt | 12 +- docs/i18n/es/llm.txt | 12 +- docs/i18n/fa/llm.txt | 12 +- docs/i18n/fi/llm.txt | 12 +- docs/i18n/fr/llm.txt | 12 +- docs/i18n/gu/llm.txt | 12 +- docs/i18n/he/llm.txt | 12 +- docs/i18n/hi/llm.txt | 12 +- docs/i18n/hu/llm.txt | 12 +- docs/i18n/id/llm.txt | 12 +- docs/i18n/in/llm.txt | 12 +- docs/i18n/it/llm.txt | 12 +- docs/i18n/ja/llm.txt | 12 +- docs/i18n/ko/llm.txt | 12 +- docs/i18n/mr/llm.txt | 12 +- docs/i18n/ms/llm.txt | 12 +- docs/i18n/nl/llm.txt | 12 +- docs/i18n/no/llm.txt | 12 +- docs/i18n/phi/llm.txt | 12 +- docs/i18n/pl/llm.txt | 12 +- docs/i18n/pt-BR/llm.txt | 12 +- docs/i18n/pt/llm.txt | 12 +- docs/i18n/ro/llm.txt | 12 +- docs/i18n/ru/llm.txt | 12 +- docs/i18n/sk/llm.txt | 12 +- docs/i18n/sv/llm.txt | 12 +- docs/i18n/sw/llm.txt | 12 +- docs/i18n/ta/llm.txt | 12 +- docs/i18n/te/llm.txt | 12 +- docs/i18n/th/llm.txt | 12 +- docs/i18n/tr/llm.txt | 12 +- docs/i18n/uk-UA/llm.txt | 12 +- docs/i18n/ur/llm.txt | 12 +- docs/i18n/vi/llm.txt | 12 +- docs/i18n/zh-CN/llm.txt | 12 +- docs/i18n/zh-TW/llm.txt | 12 +- llm.txt | 4 +- open-sse/executors/copilot-m365-connection.ts | 7 -- open-sse/executors/copilot-m365-web.ts | 8 +- open-sse/executors/index.ts | 3 +- .../handlers/chatCore/clientUsageBuffer.ts | 7 +- open-sse/utils/usageTracking.ts | 7 +- scripts/check/check-env-doc-sync.mjs | 4 - src/app/api/providers/[id]/models/route.ts | 12 +- src/app/api/v1/models/catalog.ts | 14 ++- src/app/api/v1/models/catalogResponse.ts | 2 +- src/i18n/messages/ar.json | 54 +-------- src/i18n/messages/az.json | 54 +-------- src/i18n/messages/bg.json | 54 +-------- src/i18n/messages/bn.json | 54 +-------- src/i18n/messages/cs.json | 54 +-------- src/i18n/messages/da.json | 54 +-------- src/i18n/messages/de.json | 54 +-------- src/i18n/messages/es.json | 54 +-------- src/i18n/messages/fa.json | 54 +-------- src/i18n/messages/fi.json | 54 +-------- src/i18n/messages/fr.json | 54 +-------- src/i18n/messages/gu.json | 54 +-------- src/i18n/messages/he.json | 54 +-------- src/i18n/messages/hi.json | 54 +-------- src/i18n/messages/hu.json | 54 +-------- src/i18n/messages/id.json | 54 +-------- src/i18n/messages/in.json | 54 +-------- src/i18n/messages/it.json | 54 +-------- src/i18n/messages/ja.json | 54 +-------- src/i18n/messages/ko.json | 54 +-------- src/i18n/messages/mr.json | 54 +-------- src/i18n/messages/ms.json | 54 +-------- src/i18n/messages/nl.json | 54 +-------- src/i18n/messages/no.json | 54 +-------- src/i18n/messages/phi.json | 54 +-------- src/i18n/messages/pl.json | 54 +-------- src/i18n/messages/pt-BR.json | 20 +--- src/i18n/messages/pt.json | 54 +-------- src/i18n/messages/ro.json | 54 +-------- src/i18n/messages/ru.json | 54 +-------- src/i18n/messages/sk.json | 54 +-------- src/i18n/messages/sv.json | 54 +-------- src/i18n/messages/sw.json | 54 +-------- src/i18n/messages/ta.json | 54 +-------- src/i18n/messages/te.json | 54 +-------- src/i18n/messages/th.json | 54 +-------- src/i18n/messages/tr.json | 54 +-------- src/i18n/messages/uk-UA.json | 54 +-------- src/i18n/messages/ur.json | 54 +-------- src/i18n/messages/vi.json | 9 +- src/i18n/messages/zh-CN.json | 32 +----- src/i18n/messages/zh-TW.json | 32 +----- src/lib/modelMetadataRegistry.ts | 1 + stryker.conf.json | 8 -- tests/unit/check-env-doc-sync.test.ts | 26 ----- ...context-overflow-compression-probe.test.ts | 106 ++++++++++++++++-- .../unit/db-driver-bundling-externals.test.ts | 5 +- ...ard-session-lease-bypass-inventory.test.ts | 11 +- tests/unit/sse-heartbeat.test.ts | 5 - 107 files changed, 583 insertions(+), 2313 deletions(-) delete mode 100644 changelog.d/maintenance/env-doc-sync-adhoc-bot.md delete mode 100644 changelog.d/maintenance/release-v3850-basereds-drain-20260820.md diff --git a/AGENTS.md b/AGENTS.md index f5e45cfba9..2168ae70b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below. | Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) | | Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | | Services | `open-sse/services/` | Combo routing, rate limits, caching, etc | -| Database | `src/lib/db/` | SQLite domain modules (155 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (154 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/README.md b/README.md index 6809b92e9f..fc66a61a71 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ | | v3.8.49 | **v3.8.50** | `v3.8.51+` | | ------------------------- | :-----: | :---------: | :---------: | -| 🌐 Providers | 290 | **343** | more queued | +| 🌐 Providers | 290 | **342** | more queued | | 🧠 Documented models | 1185 | **1202** | — | | 🖼️ Modality Bridge | — | 🆕 vision | video | | 📡 Radar free catalog | — | 🆕 opt-in | — | @@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
-The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 57 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests). +The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests).

@@ -646,7 +646,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) -> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **57 free forever**. +> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **56 free forever**.
@@ -1174,7 +1174,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 155 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 154 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) diff --git a/changelog.d/maintenance/env-doc-sync-adhoc-bot.md b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md deleted file mode 100644 index dbd759be29..0000000000 --- a/changelog.d/maintenance/env-doc-sync-adhoc-bot.md +++ /dev/null @@ -1 +0,0 @@ -- **chore(ci):** ignore ad-hoc `BOT_TOKEN`/`BOT_URL` in env-doc-sync (scripts/ad-hoc mesh helpers, not runtime config) diff --git a/changelog.d/maintenance/release-v3850-basereds-drain-20260820.md b/changelog.d/maintenance/release-v3850-basereds-drain-20260820.md deleted file mode 100644 index 5964e9f9a7..0000000000 --- a/changelog.d/maintenance/release-v3850-basereds-drain-20260820.md +++ /dev/null @@ -1 +0,0 @@ -- **fix(ci):** drain shared release/v3.8.50 base-reds that were failing every PR merge-ref (public-creds M365 pasted apiKey, Stryker covering tests, dead-code 419, i18n key parity, CC models listing 400 before cache, SSE comment heartbeat tests, all-zero usage estimate path, catalog builder yield every entry, leftover #10225/#10503 hard-overflow tests after #10162 advisory estimates). diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index ac3a3e598a..eae9067b4b 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -102,9 +102,8 @@ "_rebaseline_2026_07_28_v3849_release": "75.5 -> 99 (+23.5). Aperto EXIGIDO pelo modo --require-tighten do ratchet: a métrica melhorou de verdade no ciclo v3.8.49. A causa é o workflow assíncrono de tradução, que finalmente alcançou o denominador em EN — as rebaselines anteriores (v3.8.39/.44/.47) foram todas afrouxamentos registrando o atraso das traduções, e agora ele foi pago. O coletor SUBTRAI os placeholders (present - placeholder em scripts/quality/collect-metrics.mjs), então os 317 marcadores __MISSING__ que esta release introduziu para o drift de valor já estão descontados dos 99 — o número é honesto, não inflado por placeholder. Medido pelo collect-metrics do CI no run 30404226939." }, "deadExports": { - "value": 419, + "value": 415, "direction": "down", - "_rebaseline_2026_08_20_v3850_knip_cycle": "415 -> 419. Measured by CI check:dead-code on release/v3.8.50 merge refs (DEAD_TOTAL=419). Inherited cycle knip drift; structural cleanup remains separate debt.", "_rebaseline_2026_08_09_v3850_post_sweep": "227 -> 230. Measured by npm run check:dead-code on the unmodified release/v3.8.50 tip 382449d593 during the mandatory --full-ci pre-flight. The +3 is inherited cycle drift from the authorized merge sweep; this repair adds no production exports. Rebaseline records the actual tip so ci.yml quality-gate can run, while structural cleanup remains separate debt.", "_rebaseline_2026_07_01_v3843_release": "225->227 (+2). v3.8.43 cycle drift, surfaced in the Quality Ratchet job after eslintWarnings was rebaselined (check:dead-code runs there). 227 = measured by check:dead-code (knip) on the release tip 4635076eb. The 5 CI fixes add 0 dead exports: safeHttpHref in linkify.ts is module-local AND used (called by linkifyText); no new exports; test files are not scanned. Tighten via --update next cycle.", "dedicatedGate": true, diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg index 445d06e651..c73ef5e0d7 100644 --- a/docs/diagrams/promise-pillars.svg +++ b/docs/diagrams/promise-pillars.svg @@ -1,4 +1,4 @@ - + Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle. diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index e08675b921..732d79189c 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index 444523257e..e930a1f05a 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 444523257e..e930a1f05a 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 821826af9d..baa6656839 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index df907c345b..dfb5f9b2b8 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index 5dc69bbfa2..10ce4811ac 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index dc22417c1a..b5ebeb9c86 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index 40ec1505b8..72d4fa4a05 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 868685a3fc..4b324a992d 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 80f3d1041d..92d30e036c 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index cfaf0a2743..2b5cdefa71 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 44592006cb..d0f0f42f59 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index 30a05553b6..f8f0b3f644 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index 60f431d7bd..d79717b61e 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 8e2250cad7..1b941f1e5a 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index 8bfe331767..e1b23c5e3d 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 71d5aabf10..7713dacb68 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 48543ecaed..e0823ba499 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 8aa97b8d4d..5c1963195f 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 83d636964a..2ae8fe4ec4 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 60c5ab5b0d..157eec643a 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index 64321ca32b..e26b195208 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index 0ebbac8674..67f935d590 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index e93c917dba..e28dda1508 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index d27115a657..ed539c84be 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index 6c2c3c80cf..f8665fa410 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index 572bdc306c..c56d966794 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index aa0d7cb727..6e2aa2bb20 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index 475022ba34..4fac614baf 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 338cdf40c6..9d522135b2 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index fd92eef237..df0dbd95a7 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 3b990bf66f..59246e38bf 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index f93f896f0d..dbbf236c5d 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index 4a198c6e93..842ca8c7ee 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index de9669853d..5098dbc0ae 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index 73e23cbcf8..f1251db7be 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 6745e5ba3b..ba729555c2 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 29c2e9a6ee..33b12b870b 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index b7cafd24a9..bd5d5fdc83 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index e8b3a80a5b..4b59fd5d3d 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 786a99948f..122734bec7 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index 5610d851dc..b1f841f8e9 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **342 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/llm.txt b/llm.txt index ae739af5a7..c80df65ca7 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 155 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 155 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/open-sse/executors/copilot-m365-connection.ts b/open-sse/executors/copilot-m365-connection.ts index 1f9563ef46..d5f6d80c6c 100644 --- a/open-sse/executors/copilot-m365-connection.ts +++ b/open-sse/executors/copilot-m365-connection.ts @@ -117,13 +117,6 @@ export function newChatSessionId(): string { return randomBytes(16).toString("hex"); } -/** Inverse of parsePastedCredential for persisting a refreshed token. */ -export function formatPastedM365ApiKey(accessToken: string, chathubPath: string): string { - const tokenField = ["access", "token"].join("_"); - const pathField = "chathubPath"; - return `${tokenField}=${accessToken}; ${pathField}=${chathubPath}`; -} - function parsePastedCredential( raw: string ): Partial> { diff --git a/open-sse/executors/copilot-m365-web.ts b/open-sse/executors/copilot-m365-web.ts index 97cdd011c8..57f854ef86 100644 --- a/open-sse/executors/copilot-m365-web.ts +++ b/open-sse/executors/copilot-m365-web.ts @@ -8,7 +8,6 @@ import { currentM365AccessToken, currentM365ChathubPath, decodeJwtClaims, - formatPastedM365ApiKey, redactWsUrl, refreshM365AccessToken, resolveConnectionParams, @@ -321,16 +320,15 @@ export class CopilotM365WebExecutor extends BaseExecutor { const rotated = result.refreshToken || refreshToken; const chathubPath = currentM365ChathubPath(credentials); - const pastedApiKey = chathubPath - ? formatPastedM365ApiKey(result.accessToken, chathubPath) - : undefined; const next = { ...credentials, accessToken: result.accessToken, refreshToken: rotated, // Keep the pasted-format apiKey self-consistent so every resolution path // (fresh column, stale column, dashboard re-read) sees the same token. - ...(pastedApiKey ? { apiKey: pastedApiKey } : {}), + ...(chathubPath + ? { apiKey: `access_token=${result.accessToken}; chathubPath=${chathubPath}` } + : {}), ...(result.expiresIn ? { expiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString() } : {}), diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 98aa665ca0..d82452bb7e 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -1,5 +1,4 @@ import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts"; -import type { BaseExecutor } from "./base.ts"; import { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor } from "./registry.ts"; import { AntigravityExecutor } from "./antigravity.ts"; import { GithubExecutor } from "./github.ts"; @@ -234,7 +233,7 @@ const executors = { // Bootstrap: register every built-in in the ExecutorRegistry. registerExecutor // throws on duplicates, so an alias collision fails at module load, exactly as // loudly as a duplicate object key would have failed at lint time. -for (const [alias, executor] of Object.entries(executors) as Array<[string, BaseExecutor]>) { +for (const [alias, executor] of Object.entries(executors)) { registerExecutor(alias, executor); } diff --git a/open-sse/handlers/chatCore/clientUsageBuffer.ts b/open-sse/handlers/chatCore/clientUsageBuffer.ts index 6c160ef36d..4c7b1e48dc 100644 --- a/open-sse/handlers/chatCore/clientUsageBuffer.ts +++ b/open-sse/handlers/chatCore/clientUsageBuffer.ts @@ -104,12 +104,7 @@ export function applyClientUsageBuffer( deps: ClientUsageBufferDeps = DEFAULT_DEPS ): void { const { preserveContextBudgetInVisibleUsage = false } = options; - // All-zero usage stubs must take the estimate path. sanitizeProviderUsageForRequest - // (#10705) rewrites a 0 input count on a non-trivial body into a local estimate, - // which would make isEmptyUsage false and then addBufferToUsage turn zeros into - // USAGE_TOKEN_BUFFER. - const usageIsEmpty = isEmptyUsage(translatedResponse?.usage); - if (translatedResponse?.usage && !usageIsEmpty) { + if (translatedResponse?.usage) { translatedResponse.usage = sanitizeProviderUsageForRequest( translatedResponse.usage, body, diff --git a/open-sse/utils/usageTracking.ts b/open-sse/utils/usageTracking.ts index 151a5f6d8d..c89527518e 100644 --- a/open-sse/utils/usageTracking.ts +++ b/open-sse/utils/usageTracking.ts @@ -449,12 +449,11 @@ function resolveUsageFormat(usage: UsageLike | null | undefined, targetFormat: s function getReportedInputTokens(usage: UsageLike, format: string): number { if (format === FORMATS.CLAUDE) { - const claudeInput = + return ( tokenNumber(usage.input_tokens) + tokenNumber(usage.cache_read_input_tokens) + - tokenNumber(usage.cache_creation_input_tokens); - if (claudeInput > 0) return claudeInput; - return tokenNumber(usage.prompt_tokens); + tokenNumber(usage.cache_creation_input_tokens) + ); } if (format === FORMATS.GEMINI) { return tokenNumber(usage.promptTokenCount); diff --git a/scripts/check/check-env-doc-sync.mjs b/scripts/check/check-env-doc-sync.mjs index 097b24c60e..1fba62bfd5 100644 --- a/scripts/check/check-env-doc-sync.mjs +++ b/scripts/check/check-env-doc-sync.mjs @@ -126,10 +126,6 @@ const IGNORE_FROM_CODE = new Set([ // ("http://192.168.0.15:20128" / null), never OmniRoute runtime config (#5151). "COMBO_LIVE_BASE_URL", "COMBO_LIVE_API_KEY", - // Ad-hoc mesh/coverage scripts under scripts/ad-hoc/*.mjs (mesh-send, mesh-run, - // verify-coverage). Operator-supplied script secrets, not OmniRoute runtime config. - "BOT_TOKEN", - "BOT_URL", // Homologation E2E suite (npm run homolog) vars — configured via the dedicated // .env.homolog file (template: .env.homolog.example), never in the runtime .env. // Test/ops-only signals against the homologation VPS, same class as COMBO_LIVE_*. diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 217ee18dc9..818156aaec 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -1882,6 +1882,12 @@ export async function GET( } if (isAnthropicCompatibleProvider(provider)) { + const cachedResponse = maybeReturnCachedDiscovery(); + if (cachedResponse) return cachedResponse; + + const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled(); + if (autoFetchDisabledResponse) return autoFetchDisabledResponse; + if (isClaudeCodeCompatibleProvider(provider)) { return NextResponse.json( { error: `Provider ${provider} does not support models listing` }, @@ -1889,12 +1895,6 @@ export async function GET( ); } - const cachedResponse = maybeReturnCachedDiscovery(); - if (cachedResponse) return cachedResponse; - - const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled(); - if (autoFetchDisabledResponse) return autoFetchDisabledResponse; - let baseUrl = getProviderBaseUrl(connection.providerSpecificData); if (!baseUrl) { const fallback = buildDiscoveryFallbackResponse({ diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index ff3437c974..d4797775db 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -241,7 +241,7 @@ async function buildUnifiedModelsResponseCore( // event-loop yield, so a large deployment pins the single Node.js thread for the // whole build (reporter: 183 connections / 2000+ models → 10.1s stall that blocks the // dashboard WS heartbeat). Yield every `catYIELD_EVERY` items across the hot loops. - const catYIELD_EVERY = 1; + const catYIELD_EVERY = 20; let catYieldCount = 0; const maybeYieldCatalogBuild = async (): Promise => { catYieldCount++; @@ -511,11 +511,13 @@ async function buildUnifiedModelsResponseCore( const targetModel = getComboTargetModelId(target); if (!targetModel) return null; - const canonical = getCanonicalModelMetadata({ - provider: targetModel.providerId, - model: targetModel.modelId, - snapshot: capabilityResolutionSnapshot, - }); + const canonical = getCanonicalModelMetadata( + { + provider: targetModel.providerId, + model: targetModel.modelId, + }, + capabilityResolutionSnapshot + ); if (!canonical) return null; const providerId = canonical.provider || targetModel.providerId; diff --git a/src/app/api/v1/models/catalogResponse.ts b/src/app/api/v1/models/catalogResponse.ts index eab83ddd0c..198a5a6d30 100644 --- a/src/app/api/v1/models/catalogResponse.ts +++ b/src/app/api/v1/models/catalogResponse.ts @@ -229,7 +229,7 @@ export async function finalizeCatalogResponse( await yieldTurn(); const capabilityResolutionSnapshot = createModelCapabilityResolutionSnapshot(); const enriched: Array> = []; - const catYIELD_EVERY = 1; + const catYIELD_EVERY = 5; let catEnrichCount = 0; for (const model of finalModels) { let listedModel: Record; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 8161057aca..329df3d883 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -3718,11 +3718,7 @@ "errorDescription": "لم نتمكن من تحميل بيانات المجموعة في الوقت الحالي. تحقق من اتصالك وحاول مرة أخرى.", "errorId": "معرّف الخطأ: {id}", "errorRetry": "حاول مرة أخرى", - "comboLabel": "كومبو", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "كومبو" }, "costs": { "title": "التكاليف", @@ -6220,8 +6216,7 @@ "kiro": "الفئة المجانية: 50 رصيدًا شهريًا (~25 ألف–100 ألف توكن). ⚠️ تحظر شروط خدمة Kiro استخدام وكيل/أداة خارجية.", "codex": "ربط OpenAI Codex باستخدام تدفق OAuth الحالي.", "qwen": "ربط Qwen Code باستخدام تدفق OAuth الحالي.", - "github-models": "أنشئ رمز وصول شخصي (PAT) لـ GitHub بنطاق 'models: read' على github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "أنشئ رمز وصول شخصي (PAT) لـ GitHub بنطاق 'models: read' على github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} يقبل معرفات النماذج الأصلية للموفر. قم بالاستيراد من /models أو قم بإضافة معرفات مخصصة للتوجيه.", "bedrockModelsDescription": "يتم تحديد نطاق نماذج Amazon Bedrock حسب منطقة AWS. قم بالاستيراد من /models أو قم بإضافة معرفات نماذج Bedrock الممكنة في المنطقة المحددة.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK مقتضب (文言)", "description": "أسلوب صيني كلاسيكي فائق الاقتضاب (متاح للغة الصينية فقط)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "انتظر فترة التهدئة", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "الصحة", "cliproxyapiPort": "منفذ", "qdrantHost": "مضيف", - "qdrantCollection": "مجموعة", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "مجموعة" }, "contextRtk": { "title": "محرك آر تي كيه", @@ -8637,28 +8624,7 @@ "saved": "تم الحفظ.", "saveFailed": "تعذر الحفظ.", "enableAria": "تمكين محرك OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "الخدمات المضمنة", @@ -9538,17 +9504,7 @@ "updatedShort": "تم التحديث", "lastRefreshed": "آخر تحديث", "providerQuota": "حصة المزود", - "providerQuotaHomeHint": "الحالة المباشرة عبر الحسابات المتصلة", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "الحالة المباشرة عبر الحسابات المتصلة" }, "modals": { "waitingAuth": "في انتظار التصريح", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index e340292765..7c7754b05e 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -3718,11 +3718,7 @@ "errorDescription": "Hazırda kombinasiyalı məlumatları yükləyə bilmirik. Bağlantınızı yoxlayın və yenidən cəhd edin.", "errorId": "Xəta ID: {id}", "errorRetry": "Yenidən Cəhd Et", - "comboLabel": "Kombinasiya", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinasiya" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "github.com/settings/tokens ünvanında 'models: read' əhatə dairəsi ilə GitHub PAT yaradın", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens ünvanında 'models: read' əhatə dairəsi ilə GitHub PAT yaradın" }, "passthroughModelsDescription": "{provider} provayderin yerli model identifikatorlarını qəbul edir. /modellərdən idxal edin və ya marşrutlaşdırma üçün fərdi identifikatorlar əlavə edin.", "bedrockModelsDescription": "Amazon Bedrock modelləri AWS bölgəsi tərəfindən əhatə olunur. /modellərdən idxal edin və ya seçilmiş regionda aktivləşdirilmiş Bedrock model ID-lərini əlavə edin.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Yığcam CJK (文言)", "description": "Klassik Çin ultra-yığcam üslubu (yalnız Çin dili üçün əlçatandır)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Cooldown üçün gözləyin", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Sağlamlıq", "cliproxyapiPort": "Port", "qdrantHost": "Ev sahibi", - "qdrantCollection": "Kolleksiya", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Kolleksiya" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Saxlanıldı.", "saveFailed": "Saxlamaq mümkün olmadı.", "enableAria": "OmniGlyph mühərrikini aktivləşdir", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Quraşdırılmış xidmətlər", @@ -9538,17 +9504,7 @@ "updatedShort": "Yeniləndi", "lastRefreshed": "Sonuncu dəfə yenilənib", "providerQuota": "Provayder kvotası", - "providerQuotaHomeHint": "Qoşulmuş hesablar üzrə canlı status", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Qoşulmuş hesablar üzrə canlı status" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 58099b4fae..463943e63a 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -3718,11 +3718,7 @@ "errorDescription": "Не успяхме да заредим данните за комбинирането в момента. Проверете връзката си и опитайте отново.", "errorId": "Идентификатор на грешка: {id}", "errorRetry": "Опитай отново", - "comboLabel": "Комбо", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Комбо" }, "costs": { "title": "Разходи", @@ -6220,8 +6216,7 @@ "kiro": "Безплатен план: 50 кредита/месец (~25K–100K токена). ⚠️ Условията за ползване на Kiro забраняват използването на прокси/инструменти от трети страни.", "codex": "Свързване на OpenAI Codex със съществуващия OAuth поток.", "qwen": "Свързване на Qwen Code със съществуващия OAuth поток.", - "github-models": "Създайте GitHub PAT с обхват 'models: read' на github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Създайте GitHub PAT с обхват 'models: read' на github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} приема собствени идентификатори на модела на доставчика. Импортирайте от /models или добавете персонализирани идентификатори за маршрутизиране.", "bedrockModelsDescription": "Моделите на Amazon Bedrock са обхванати от регион на AWS. Импортирайте от /models или добавете идентификатори на модел Bedrock, активирани в избрания регион.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Сбит CJK (文言)", "description": "Класически китайски ултра-сбит стил (наличен само за китайски)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Изчакайте Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Здраве", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Колекция", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Колекция" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Запазено.", "saveFailed": "Неуспешно запазване.", "enableAria": "Активиране на енджина OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Вградени услуги", @@ -9538,17 +9504,7 @@ "updatedShort": "Актуализирано", "lastRefreshed": "Последно опреснено", "providerQuota": "Квота на доставчика", - "providerQuotaHomeHint": "Статус в реално време за свързаните акаунти", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Статус в реално време за свързаните акаунти" }, "modals": { "waitingAuth": "Изчакване на разрешение", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 13600ef639..fe486a3ffd 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -3718,11 +3718,7 @@ "errorDescription": "আমরা এখন কম্বো ডেটা লোড করতে পারিনি। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।", "errorId": "ত্রুটি আইডি: {id}", "errorRetry": "পুনরায় চেষ্টা করুন", - "comboLabel": "কম্বো", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "কম্বো" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "ফ্রি টিয়ার: 50 ক্রেডিট/মাস (~25K–100K টোকেন)। ⚠️ Kiro ToS তৃতীয় পক্ষের প্রক্সি/হারনেস ব্যবহার নিষিদ্ধ করে।", "codex": "বিদ্যমান OAuth ফ্লো-এর সাথে OpenAI Codex সংযুক্ত করুন।", "qwen": "বিদ্যমান OAuth ফ্লো-এর সাথে Qwen Code সংযুক্ত করুন।", - "github-models": "github.com/settings/tokens-এ 'models: read' স্কোপ সহ একটি GitHub PAT তৈরি করুন", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens-এ 'models: read' স্কোপ সহ একটি GitHub PAT তৈরি করুন" }, "passthroughModelsDescription": "{provider} প্রদানকারী-নেটিভ মডেল আইডি গ্রহণ করে। /মডেল থেকে আমদানি করুন বা রাউটিং এর জন্য কাস্টম আইডি যোগ করুন।", "bedrockModelsDescription": "অ্যামাজন বেডরক মডেলগুলি AWS অঞ্চল দ্বারা স্কোপ করা হয়েছে৷ /মডেল থেকে আমদানি করুন বা নির্বাচিত অঞ্চলে সক্ষম বেডরক মডেল আইডি যোগ করুন।", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "সংক্ষিপ্ত CJK (文言)", "description": "ক্লাসিক্যাল-চাইনিজ অতি-সংক্ষিপ্ত শৈলী (শুধুমাত্র চাইনিজ ভাষার জন্য উপলব্ধ)।" - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "কুলডাউনের জন্য অপেক্ষা করুন", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "স্বাস্থ্য", "cliproxyapiPort": "পোর্ট", "qdrantHost": "হোস্ট", - "qdrantCollection": "সংগ্রহ", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "সংগ্রহ" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "সংরক্ষিত হয়েছে।", "saveFailed": "সংরক্ষণ করা যায়নি।", "enableAria": "OmniGlyph ইঞ্জিনটি সক্রিয় করুন", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "এমবেডেড সেবাসমূহ", @@ -9538,17 +9504,7 @@ "updatedShort": "আপডেট করা হয়েছে", "lastRefreshed": "সর্বশেষ রিফ্রেশ করা হয়েছে", "providerQuota": "প্রোভাইডার কোটা", - "providerQuotaHomeHint": "সংযুক্ত অ্যাকাউন্টগুলোর লাইভ স্ট্যাটাস", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "সংযুক্ত অ্যাকাউন্টগুলোর লাইভ স্ট্যাটাস" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index cb6e74098a..22139ce32a 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -3718,11 +3718,7 @@ "errorDescription": "Nyní se nám nepodařilo načíst data pro kombinaci. Zkontrolujte své připojení a zkuste to znovu.", "errorId": "Chyba ID: {id}", "errorRetry": "Zkusit znovu", - "comboLabel": "Kombinace", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinace" }, "costs": { "title": "Náklady", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Připojit OpenAI Codex pomocí stávajícího toku OAuth.", "qwen": "Připojit Qwen Code pomocí stávajícího toku OAuth.", - "github-models": "Vytvořte GitHub PAT s rozsahem 'models: read' na github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Vytvořte GitHub PAT s rozsahem 'models: read' na github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} přijímá ID modelu nativního poskytovatele. Importujte z /models nebo přidejte vlastní ID pro směrování.", "bedrockModelsDescription": "Modely Amazon Bedrock jsou vymezeny podle regionu AWS. Importujte z /models nebo přidejte ID modelu Bedrock povolené ve vybrané oblasti.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Stručné CJK (文言)", "description": "Klasický čínský ultra stručný styl (k dispozici pouze pro čínštinu)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Počkejte na Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Zdraví", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Kolekce", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Kolekce" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Uloženo.", "saveFailed": "Nepodařilo se uložit.", "enableAria": "Povolit engine OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Vestavěné služby", @@ -9538,17 +9504,7 @@ "updatedShort": "Aktualizováno", "lastRefreshed": "Naposledy aktualizováno", "providerQuota": "Kvóta poskytovatele", - "providerQuotaHomeHint": "Aktuální stav napříč připojenými účty", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Aktuální stav napříč připojenými účty" }, "modals": { "waitingAuth": "Očekávám Autorizaci", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 7c746db1a7..cbd74f9a49 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -3718,11 +3718,7 @@ "errorDescription": "Vi kunne ikke indlæse kombinationsdata lige nu. Tjek din forbindelse og prøv igen.", "errorId": "Fejl ID: {id}", "errorRetry": "Prøv igen", - "comboLabel": "Kombination", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombination" }, "costs": { "title": "Omkostninger", @@ -6220,8 +6216,7 @@ "kiro": "Gratis niveau: 50 kreditter/måned (~25K–100K tokens). ⚠️ Kiro ToS forbyder brug af tredjeparts proxy/harness.", "codex": "Forbind OpenAI Codex med det eksisterende OAuth-flow.", "qwen": "Forbind Qwen Code med det eksisterende OAuth-flow.", - "github-models": "Opret et GitHub PAT med 'models: read'-scope på github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Opret et GitHub PAT med 'models: read'-scope på github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} accepterer udbyder-native model-id'er. Importer fra /models eller tilføj brugerdefinerede id'er til routing.", "bedrockModelsDescription": "Amazon Bedrock-modeller er omfattet af AWS-regionen. Importer fra /models eller tilføj Bedrock-model-id'er aktiveret i det valgte område.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Kortfattet CJK (文言)", "description": "Klassisk kinesisk ultra-kortfattet stil (kun tilgængelig for kinesisk)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Vent på nedkøling", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Sundhed", "cliproxyapiPort": "Port", "qdrantHost": "Vært", - "qdrantCollection": "Samling", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Samling" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Gemt.", "saveFailed": "Kunne ikke gemme.", "enableAria": "Aktiver OmniGlyph-motoren", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Indlejrede tjenester", @@ -9538,17 +9504,7 @@ "updatedShort": "Opdateret", "lastRefreshed": "Sidst opdateret", "providerQuota": "Udbyderkvote", - "providerQuotaHomeHint": "Livestatus på tværs af tilknyttede konti", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Livestatus på tværs af tilknyttede konti" }, "modals": { "waitingAuth": "Venter på autorisation", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index c7702188ae..fb4e14b485 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -3718,11 +3718,7 @@ "errorDescription": "Wir konnten die Kombinationsdaten momentan nicht laden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.", "errorId": "Fehler-ID: {id}", "errorRetry": "Versuche es erneut", - "comboLabel": "Kombination", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombination" }, "costs": { "title": "Kosten", @@ -6220,8 +6216,7 @@ "kiro": "Kostenlose Stufe: 50 Credits/Monat (~25K–100K Token). ⚠️ Die Nutzungsbedingungen von Kiro verbieten die Nutzung von Drittanbieter-Proxys/Harnesses.", "codex": "OpenAI Codex mit dem bestehenden OAuth-Flow verbinden.", "qwen": "Qwen Code mit dem bestehenden OAuth-Flow verbinden.", - "github-models": "Erstellen Sie einen GitHub-PAT mit dem Scope 'models: read' unter github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Erstellen Sie einen GitHub-PAT mit dem Scope 'models: read' unter github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} akzeptiert provider-native Modell-IDs. Importiere sie über /models oder füge eigene IDs fürs Routing hinzu.", "bedrockModelsDescription": "Amazon-Bedrock-Modelle hängen von der AWS-Region ab. Importiere sie über /models oder füge Bedrock-Modell-IDs hinzu, die in der gewählten Region aktiviert sind.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Knappe CJK (文言)", "description": "Klassisch-chinesischer, extrem knapper Stil (nur für Chinesisch verfügbar)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Warten Sie auf die Abklingzeit", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Gesundheit", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Sammlung", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Sammlung" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Gespeichert.", "saveFailed": "Konnte nicht gespeichert werden.", "enableAria": "OmniGlyph-Engine aktivieren", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Eingebettete Dienste", @@ -9538,17 +9504,7 @@ "updatedShort": "Aktualisiert", "lastRefreshed": "Zuletzt aktualisiert", "providerQuota": "Anbieter-Kontingent", - "providerQuotaHomeHint": "Live-Status über verbundene Konten hinweg", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Live-Status über verbundene Konten hinweg" }, "modals": { "waitingAuth": "Warten auf Autorisierung", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 1f4c9dc5d7..6978c01a41 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -3718,11 +3718,7 @@ "errorDescription": "No pudimos cargar los datos del combo en este momento. Verifica tu conexión y vuelve a intentarlo.", "errorId": "Error ID: {id}", "errorRetry": "Inténtalo de nuevo", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Costos", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} acepta ID de modelo nativo del proveedor. Importe desde /models o agregue ID personalizados para enrutamiento.", "bedrockModelsDescription": "Los modelos de Amazon Bedrock tienen como alcance la región de AWS. Importe desde /models o agregue ID de modelo Bedrock habilitados en la región seleccionada.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Terse CJK (文言)", "description": "Classical-Chinese ultra-terse style (available only for Chinese)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Esperar a que se enfríe", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Salud", "cliproxyapiPort": "Puerto", "qdrantHost": "Anfitrión", - "qdrantCollection": "Colección", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Colección" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Saved.", "saveFailed": "Could not save.", "enableAria": "Enable the OmniGlyph engine", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Embedded Services", @@ -9538,17 +9504,7 @@ "updatedShort": "Updated", "lastRefreshed": "Last refreshed", "providerQuota": "Provider Quota", - "providerQuotaHomeHint": "Live status across connected accounts", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Live status across connected accounts" }, "modals": { "waitingAuth": "Esperando autorización", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 12659ec986..9962332860 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -3718,11 +3718,7 @@ "errorDescription": "در حال حاضر نمی‌توانیم داده‌های ترکیبی را بارگذاری کنیم. اتصال خود را بررسی کنید و دوباره تلاش کنید.", "errorId": "شناسه خطا: {id}", "errorRetry": "دوباره تلاش کنید", - "comboLabel": "ترکیب", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "ترکیب" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "طرح رایگان: ۵۰ اعتبار/ماه (~۲۵ هزار–۱۰۰ هزار توکن). ⚠️ شرایط خدمات Kiro استفاده از پروکسی/هارنس شخص ثالث را ممنوع می‌کند.", "codex": "اتصال OpenAI Codex با جریان OAuth موجود.", "qwen": "اتصال Qwen Code با جریان OAuth موجود.", - "github-models": "یک GitHub PAT با محدوده (scope) 'models: read' در github.com/settings/tokens ایجاد کنید", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "یک GitHub PAT با محدوده (scope) 'models: read' در github.com/settings/tokens ایجاد کنید" }, "passthroughModelsDescription": "{provider} شناسه های مدل بومی ارائه دهنده را می پذیرد. از /models وارد کنید یا شناسه های سفارشی را برای مسیریابی اضافه کنید.", "bedrockModelsDescription": "مدل‌های بستر آمازون بر اساس منطقه AWS تعیین می‌شوند. از /models وارد کنید یا شناسه‌های مدل Bedrock را که در منطقه انتخابی فعال شده است اضافه کنید.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK موجز (文言)", "description": "سبک فوق‌موجز چینی کلاسیک (فقط برای زبان چینی در دسترس است)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "منتظر Cooldown باشید", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "سلامت", "cliproxyapiPort": "پورت", "qdrantHost": "میزبان", - "qdrantCollection": "مجموعه", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "مجموعه" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "ذخیره شد.", "saveFailed": "ذخیره نشد.", "enableAria": "فعال‌سازی موتور OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "سرویس‌های تعبیه‌شده", @@ -9538,17 +9504,7 @@ "updatedShort": "به‌روزرسانی شد", "lastRefreshed": "آخرین به‌روزرسانی", "providerQuota": "سهمیه ارائه‌دهنده", - "providerQuotaHomeHint": "وضعیت لحظه‌ای در حساب‌های متصل", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "وضعیت لحظه‌ای در حساب‌های متصل" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index e28bcc1a46..6b882ce670 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -3718,11 +3718,7 @@ "errorDescription": "Emme voi ladata yhdistelmädataa juuri nyt. Tarkista yhteytesi ja yritä uudelleen.", "errorId": "Virhe ID: {id}", "errorRetry": "Yritä uudelleen", - "comboLabel": "Yhdistelmä", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Yhdistelmä" }, "costs": { "title": "Kustannukset", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Yhdistä OpenAI Codex olemassa olevalla OAuth-työnkululla.", "qwen": "Yhdistä Qwen Code olemassa olevalla OAuth-työnkululla.", - "github-models": "Luo GitHub PAT -tunniste 'models: read' -käyttöoikeudella osoitteessa github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Luo GitHub PAT -tunniste 'models: read' -käyttöoikeudella osoitteessa github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} hyväksyy palveluntarjoajan alkuperäiset mallitunnukset. Tuo /modelsista tai lisää mukautettuja tunnuksia reititystä varten.", "bedrockModelsDescription": "Amazon Bedrock -mallit on luokiteltu AWS-alueen mukaan. Tuo osoitteesta /models tai lisää valitulla alueella käytössä olevat kallioperän mallitunnukset.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Tiivis CJK (文言)", "description": "Klassisen kiinan ultra-tiivis tyyli (saatavilla vain kiinaksi)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Odota jäähdytystä", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Terveys", "cliproxyapiPort": "Portti", "qdrantHost": "Isäntä", - "qdrantCollection": "Kokoelma", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Kokoelma" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Tallennettu.", "saveFailed": "Tallennus epäonnistui.", "enableAria": "Ota OmniGlyph-moottori käyttöön", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Upotetut palvelut", @@ -9538,17 +9504,7 @@ "updatedShort": "Päivitetty", "lastRefreshed": "Viimeksi päivitetty", "providerQuota": "Tarjoajan kiintiö", - "providerQuotaHomeHint": "Reaaliaikainen tila yhdistetyissä tileissä", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Reaaliaikainen tila yhdistetyissä tileissä" }, "modals": { "waitingAuth": "Odotetaan valtuutusta", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 2472ed509b..84ffa972ec 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -3718,11 +3718,7 @@ "errorDescription": "Les données des combos ne peuvent pas être chargées pour le moment. Vérifiez votre connexion et réessayez.", "errorId": "ID d'erreur : {id}", "errorRetry": "Réessayer", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Coûts", @@ -6220,8 +6216,7 @@ "kiro": "Offre gratuite : 50 crédits/mois (~25K–100K tokens). ⚠️ Les conditions d'utilisation de Kiro interdisent l'utilisation de proxys/harness tiers.", "codex": "Connecter OpenAI Codex avec le flux OAuth existant.", "qwen": "Connecter Qwen Code avec le flux OAuth existant.", - "github-models": "Créez un PAT GitHub avec la portée 'models: read' sur github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Créez un PAT GitHub avec la portée 'models: read' sur github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} accepte les ID de modèle natifs du fournisseur. Importez depuis /models ou ajoutez des ID personnalisés pour le routage.", "bedrockModelsDescription": "Les modèles Amazon Bedrock sont définis par région AWS. Importez depuis /models ou ajoutez les ID de modèle Bedrock activés dans la région sélectionnée.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK concis (文言)", "description": "Style ultra-concis en chinois classique (disponible uniquement pour le chinois)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Attendez le temps de recharge", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Santé", "cliproxyapiPort": "Port", "qdrantHost": "Hôte", - "qdrantCollection": "Collection", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Collection" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Enregistré.", "saveFailed": "Impossible d'enregistrer.", "enableAria": "Activer le moteur OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Services intégrés", @@ -9538,17 +9504,7 @@ "updatedShort": "Mis à jour", "lastRefreshed": "Dernière actualisation", "providerQuota": "Quota du fournisseur", - "providerQuotaHomeHint": "Statut en direct sur les comptes connectés", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Statut en direct sur les comptes connectés" }, "modals": { "waitingAuth": "En attente d'autorisation", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 02d9fd52aa..eb23ac5ebd 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -3718,11 +3718,7 @@ "errorDescription": "અમે હાલમાં કોમ્બો ડેટા લોડ કરી શક્યા નથી. તમારી કનેક્શન તપાસો અને ફરી પ્રયાસ કરો.", "errorId": "ભૂલ આઈડી: {id}", "errorRetry": "ફરીથી પ્રયાસ કરો", - "comboLabel": "કોમ્બો", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "કોમ્બો" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "મફત સ્તર: 50 ક્રેડિટ્સ/મહિનો (~25K–100K ટોકન્સ). ⚠️ Kiro ToS તૃતીય-પક્ષ પ્રોક્સી/હાર્નેસના ઉપયોગ પર પ્રતિબંધ મૂકે છે.", "codex": "OpenAI Codex ને હાલના OAuth ફ્લો સાથે કનેક્ટ કરો.", "qwen": "Qwen Code ને હાલના OAuth ફ્લો સાથે કનેક્ટ કરો.", - "github-models": "github.com/settings/tokens પર 'models: read' સ્કોપ સાથે GitHub PAT બનાવો", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens પર 'models: read' સ્કોપ સાથે GitHub PAT બનાવો" }, "passthroughModelsDescription": "{provider} પ્રદાતા-મૂળ મોડેલ ID સ્વીકારે છે. /મોડેલ્સમાંથી આયાત કરો અથવા રૂટીંગ માટે કસ્ટમ ID ઉમેરો.", "bedrockModelsDescription": "એમેઝોન બેડરોક મોડલ્સ AWS પ્રદેશ દ્વારા સ્કોપ્ડ છે. /મોડેલ્સમાંથી આયાત કરો અથવા પસંદ કરેલ પ્રદેશમાં સક્ષમ કરેલ બેડરોક મોડેલ ID ઉમેરો.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "સંક્ષિપ્ત CJK (文言)", "description": "ક્લાસિકલ-ચાઇનીઝ અલ્ટ્રા-સંક્ષિપ્ત શૈલી (ફક્ત ચાઇનીઝ માટે ઉપલબ્ધ)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "કૂલડાઉન માટે રાહ જુઓ", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "આરોગ્ય", "cliproxyapiPort": "પોર્ટ", "qdrantHost": "હોસ્ટ", - "qdrantCollection": "સંગ્રહ", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "સંગ્રહ" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "સાચવ્યું.", "saveFailed": "સાચવી શકાયું નથી.", "enableAria": "OmniGlyph એન્જિન સક્ષમ કરો", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "એમ્બેડેડ સેવાઓ", @@ -9538,17 +9504,7 @@ "updatedShort": "અપડેટ કરેલ", "lastRefreshed": "છેલ્લે રિફ્રેશ કરેલ", "providerQuota": "પ્રદાતા ક્વોટા", - "providerQuotaHomeHint": "કનેક્ટેડ એકાઉન્ટ્સમાં લાઇવ સ્થિતિ", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "કનેક્ટેડ એકાઉન્ટ્સમાં લાઇવ સ્થિતિ" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 04844c0408..764e30527a 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -3718,11 +3718,7 @@ "errorDescription": "לא הצלחנו לטעון את נתוני הקומבו כרגע. בדוק את החיבור שלך ונסה שוב.", "errorId": "שגיאת מזהה: {id}", "errorRetry": "נסה שוב", - "comboLabel": "קומבו", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "קומבו" }, "costs": { "title": "עלויות", @@ -6220,8 +6216,7 @@ "kiro": "מסלול חינמי: 50 קרדיטים לחודש (~25K–100K טוקנים). ⚠️ תנאי השימוש של Kiro אוסרים על שימוש בפרוקסי/harness של צד שלישי.", "codex": "חבר את OpenAI Codex באמצעות תהליך ה-OAuth הקיים.", "qwen": "חבר את Qwen Code באמצעות תהליך ה-OAuth הקיים.", - "github-models": "צור GitHub PAT עם הרשאת 'models: read' ב-github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "צור GitHub PAT עם הרשאת 'models: read' ב-github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} מקבל מזהי מודל מקוריים של ספק. ייבא מ /models או הוסף מזהים מותאמים אישית לניתוב.", "bedrockModelsDescription": "הדגמים של Amazon Bedrock נמצאים לפי אזור AWS. ייבא מ-/models או הוסף מזהי דגמי Bedrock המופעלים באזור הנבחר.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK תמציתי (文言)", "description": "סגנון סיני קלאסי אולטרה-תמציתי (זמין עבור סינית בלבד)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "המתן ל-Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "בריאות", "cliproxyapiPort": "פורט", "qdrantHost": "מארח", - "qdrantCollection": "אוסף", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "אוסף" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "נשמר.", "saveFailed": "לא ניתן היה לשמור.", "enableAria": "הפעל את מנוע OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "שירותים מובנים", @@ -9538,17 +9504,7 @@ "updatedShort": "עודכן", "lastRefreshed": "רענון אחרון", "providerQuota": "מכסת ספק", - "providerQuotaHomeHint": "סטטוס בזמן אמת בכל החשבונות המחוברים", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "סטטוס בזמן אמת בכל החשבונות המחוברים" }, "modals": { "waitingAuth": "ממתין לאישור", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 9cc3db2fa0..6591311189 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -3718,11 +3718,7 @@ "errorDescription": "हम अभी कॉम्बो डेटा लोड नहीं कर सके। कृपया अपनी कनेक्शन की जांच करें और फिर से प्रयास करें।", "errorId": "त्रुटि आईडी: {id}", "errorRetry": "फिर से प्रयास करें", - "comboLabel": "कॉम्बो", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "कॉम्बो" }, "costs": { "title": "लागत", @@ -6220,8 +6216,7 @@ "kiro": "फ्री टियर: 50 क्रेडिट/महीना (~25K–100K टोकन)। ⚠️ Kiro ToS तृतीय-पक्ष प्रॉक्सी/हार्नेस के उपयोग को प्रतिबंधित करता है।", "codex": "OpenAI Codex को मौजूदा OAuth फ़्लो से कनेक्ट करें।", "qwen": "Qwen Code को मौजूदा OAuth फ़्लो से कनेक्ट करें।", - "github-models": "github.com/settings/tokens पर 'models: read' स्कोप के साथ एक GitHub PAT बनाएं", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens पर 'models: read' स्कोप के साथ एक GitHub PAT बनाएं" }, "passthroughModelsDescription": "{provider} प्रदाता-मूल मॉडल आईडी स्वीकार करता है। /मॉडल से आयात करें या रूटिंग के लिए कस्टम आईडी जोड़ें।", "bedrockModelsDescription": "अमेज़ॅन बेडरॉक मॉडल का दायरा AWS क्षेत्र द्वारा तय किया गया है। /मॉडल से आयात करें या चयनित क्षेत्र में सक्षम बेडरॉक मॉडल आईडी जोड़ें।", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "संक्षिप्त CJK (文言)", "description": "शास्त्रीय-चीनी अति-संक्षिप्त शैली (केवल चीनी भाषा के लिए उपलब्ध)।" - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "ठंडा होने की प्रतीक्षा करें", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "स्वास्थ्य", "cliproxyapiPort": "पोर्ट", "qdrantHost": "होस्ट", - "qdrantCollection": "संग्रह", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "संग्रह" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "सहेजा गया।", "saveFailed": "सहेजा नहीं जा सका।", "enableAria": "OmniGlyph इंजन सक्षम करें", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "एम्बेडेड सेवाएँ", @@ -9538,17 +9504,7 @@ "updatedShort": "अपडेट किया गया", "lastRefreshed": "अंतिम बार रीफ़्रेश किया गया", "providerQuota": "प्रदाता कोटा", - "providerQuotaHomeHint": "कनेक्टेड खातों में लाइव स्थिति", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "कनेक्टेड खातों में लाइव स्थिति" }, "modals": { "waitingAuth": "प्राधिकरण की प्रतीक्षा की जा रही है", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index cfe296c07f..e5ccd837b3 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -3718,11 +3718,7 @@ "errorDescription": "Jelenleg nem tudtuk betölteni a kombinált adatokat. Ellenőrizze a kapcsolatát, és próbálja újra.", "errorId": "Hibaazonosító: {id}", "errorRetry": "Próbáld újra", - "comboLabel": "Kombó", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombó" }, "costs": { "title": "Költségek", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "Hozzon létre egy GitHub PAT-ot 'models: read' hatókörrel a github.com/settings/tokens oldalon", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Hozzon létre egy GitHub PAT-ot 'models: read' hatókörrel a github.com/settings/tokens oldalon" }, "passthroughModelsDescription": "Az {provider} elfogadja a szolgáltató natív modellazonosítóit. Importáljon a /models-ból, vagy adjon hozzá egyéni azonosítókat az útválasztáshoz.", "bedrockModelsDescription": "Az Amazon Bedrock modellek hatóköre az AWS régió szerint történik. Importáljon a /models mappából, vagy adja hozzá a kiválasztott régióban engedélyezett Bedrock modellazonosítókat.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Tömör CJK (文言)", "description": "Klasszikus kínai ultratömör stílus (csak kínai nyelven érhető el)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Várja meg a lehűlést", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Egészség", "cliproxyapiPort": "Port", "qdrantHost": "Gazda", - "qdrantCollection": "Gyűjtemény", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Gyűjtemény" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Mentve.", "saveFailed": "Nem sikerült menteni.", "enableAria": "Az OmniGlyph motor engedélyezése", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Beágyazott szolgáltatások", @@ -9538,17 +9504,7 @@ "updatedShort": "Frissítve", "lastRefreshed": "Legutóbb frissítve", "providerQuota": "Szolgáltatói kvóta", - "providerQuotaHomeHint": "Élő állapot a csatlakoztatott fiókokban", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Élő állapot a csatlakoztatott fiókokban" }, "modals": { "waitingAuth": "Várakozás az engedélyezésre", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index d7b86daef1..352daafd83 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -3718,11 +3718,7 @@ "errorDescription": "Kami tidak dapat memuat data kombinasi saat ini. Periksa koneksi Anda dan coba lagi.", "errorId": "Error ID: {id}", "errorRetry": "Coba Lagi", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Biaya", @@ -6220,8 +6216,7 @@ "kiro": "Tingkat gratis: 50 kredit/bulan (~25K–100K token). ⚠️ ToS Kiro melarang penggunaan proksi/harness pihak ketiga.", "codex": "Hubungkan OpenAI Codex dengan alur OAuth yang ada.", "qwen": "Hubungkan Qwen Code dengan alur OAuth yang ada.", - "github-models": "Buat GitHub PAT dengan cakupan 'models: read' di github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Buat GitHub PAT dengan cakupan 'models: read' di github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} menerima ID model asli penyedia. Impor dari /models atau tambahkan ID khusus untuk perutean.", "bedrockModelsDescription": "Model Amazon Bedrock dicakup berdasarkan wilayah AWS. Impor dari /models atau tambahkan ID model Batuan Dasar yang diaktifkan di wilayah yang dipilih.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Tionghoa Klasik (hanya tersedia untuk bahasa Tionghoa)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Tunggu Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Kesehatan", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Koleksi", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Koleksi" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Disimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Aktifkan mesin OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Layanan Tersemat", @@ -9538,17 +9504,7 @@ "updatedShort": "Diperbarui", "lastRefreshed": "Terakhir disegarkan", "providerQuota": "Kuota Penyedia", - "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung" }, "modals": { "waitingAuth": "Menunggu Otorisasi", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index bca7339449..19f45ac1ca 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -3718,11 +3718,7 @@ "errorDescription": "Kami tidak dapat memuat data kombinasi saat ini. Periksa koneksi Anda dan coba lagi.", "errorId": "ID Kesalahan: {id}", "errorRetry": "Coba Lagi", - "comboLabel": "Kombinasi", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinasi" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "Tingkat gratis: 50 kredit/bulan (~25K–100K token). ⚠️ Kiro ToS melarang penggunaan proksi/harness pihak ketiga.", "codex": "Hubungkan OpenAI Codex dengan alur OAuth yang ada.", "qwen": "Hubungkan Qwen Code dengan alur OAuth yang ada.", - "github-models": "Buat PAT GitHub dengan cakupan 'models: read' di github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Buat PAT GitHub dengan cakupan 'models: read' di github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} menerima ID model asli penyedia. Impor dari /models atau tambahkan ID khusus untuk perutean.", "bedrockModelsDescription": "Model Amazon Bedrock dicakup berdasarkan wilayah AWS. Impor dari /models atau tambahkan ID model Batuan Dasar yang diaktifkan di wilayah yang dipilih.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Tionghoa Klasik (hanya tersedia untuk bahasa Tionghoa)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Tunggu Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Kesehatan", "cliproxyapiPort": "Port", "qdrantHost": "Tuan Rumah", - "qdrantCollection": "Koleksi", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Koleksi" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Tersimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Aktifkan mesin OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Layanan Tertanam", @@ -9538,17 +9504,7 @@ "updatedShort": "Diperbarui", "lastRefreshed": "Terakhir disegarkan", "providerQuota": "Kuota Penyedia", - "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Status langsung di seluruh akun yang terhubung" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index cfdc5c16b7..7332c3206b 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -3718,11 +3718,7 @@ "errorDescription": "Non siamo riusciti a caricare i dati del combo in questo momento. Controlla la tua connessione e riprova.", "errorId": "ID Errore: {id}", "errorRetry": "Riprova", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Costi", @@ -6220,8 +6216,7 @@ "kiro": "Piano gratuito: 50 crediti/mese (~25K–100K token). ⚠️ I ToS di Kiro vietano l'uso di proxy/harness di terze parti.", "codex": "Connetti OpenAI Codex al flusso OAuth esistente.", "qwen": "Connetti Qwen Code al flusso OAuth esistente.", - "github-models": "Crea un PAT di GitHub con ambito 'models: read' su github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Crea un PAT di GitHub con ambito 'models: read' su github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} accetta ID modello nativi del provider. Importa da /modelli o aggiungi ID personalizzati per il routing.", "bedrockModelsDescription": "I modelli Amazon Bedrock hanno come ambito la regione AWS. Importa da /models o aggiungi gli ID modello Bedrock abilitati nella regione selezionata.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK conciso (文言)", "description": "Stile ultra-conciso in cinese classico (disponibile solo per il cinese)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Attendi il raffreddamento", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Salute", "cliproxyapiPort": "Porta", "qdrantHost": "Host", - "qdrantCollection": "Collezione", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Collezione" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Salvato.", "saveFailed": "Impossibile salvare.", "enableAria": "Abilita il motore OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Servizi integrati", @@ -9538,17 +9504,7 @@ "updatedShort": "Aggiornato", "lastRefreshed": "Ultimo aggiornamento", "providerQuota": "Quota del provider", - "providerQuotaHomeHint": "Stato in tempo reale tra gli account connessi", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Stato in tempo reale tra gli account connessi" }, "modals": { "waitingAuth": "In attesa di autorizzazione", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 7b86be7adb..a8c37c9eec 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -3718,11 +3718,7 @@ "errorDescription": "現在、コンボデータを読み込むことができません。接続を確認して、再試行してください。", "errorId": "エラー ID: {id}", "errorRetry": "もう一度試してください", - "comboLabel": "コンボ", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "コンボ" }, "costs": { "title": "コスト", @@ -6220,8 +6216,7 @@ "kiro": "無料枠: 50クレジット/月(約25K〜100Kトークン)。⚠️ Kiroの利用規約(ToS)は、サードパーティのプロキシやハーネスの使用を禁止しています。", "codex": "既存のOAuthフローを使用してOpenAI Codexに接続します。", "qwen": "既存のOAuthフローを使用してQwen Codeに接続します。", - "github-models": "github.com/settings/tokens で 'models: read' スコープを持つGitHub PATを作成", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens で 'models: read' スコープを持つGitHub PATを作成" }, "passthroughModelsDescription": "{provider} は、プロバイダーネイティブのモデル ID を受け入れます。 /models からインポートするか、ルーティング用のカスタム ID を追加します。", "bedrockModelsDescription": "Amazon Bedrock モデルは、AWS リージョンによって範囲が定められています。 /models からインポートするか、選択したリージョンで有効になっている Bedrock モデル ID を追加します。", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "簡潔なCJK (文言)", "description": "漢文の超簡潔スタイル (中国語でのみ利用可能)。" - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "クールダウンを待ちます", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "健康", "cliproxyapiPort": "ポート", "qdrantHost": "ホスト", - "qdrantCollection": "コレクション", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "コレクション" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "保存されました。", "saveFailed": "保存できませんでした。", "enableAria": "OmniGlyphエンジンを有効にする", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "組み込みサービス", @@ -9538,17 +9504,7 @@ "updatedShort": "更新済み", "lastRefreshed": "最終更新", "providerQuota": "プロバイダークォータ", - "providerQuotaHomeHint": "接続されたアカウント全体のライブステータス", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "接続されたアカウント全体のライブステータス" }, "modals": { "waitingAuth": "承認を待っています", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index fc210ab0f7..a003916a18 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -3718,11 +3718,7 @@ "errorDescription": "현재 콤보 데이터를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.", "errorId": "오류 ID: {id}", "errorRetry": "다시 시도해 주세요", - "comboLabel": "콤보", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "콤보" }, "costs": { "title": "비용", @@ -6220,8 +6216,7 @@ "kiro": "무료 티어: 월 50 크레딧(~25K–100K 토큰). ⚠️ Kiro ToS는 서드파티 프록시/하네스 사용을 금지합니다.", "codex": "기존 OAuth 흐름으로 OpenAI Codex를 연결합니다.", "qwen": "기존 OAuth 흐름으로 Qwen Code를 연결합니다.", - "github-models": "github.com/settings/tokens 에서 'models: read' 범위(scope)를 가진 GitHub PAT를 생성하세요.", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens 에서 'models: read' 범위(scope)를 가진 GitHub PAT를 생성하세요." }, "passthroughModelsDescription": "{provider}은 공급자 기본 모델 ID를 허용합니다. /models에서 가져오거나 라우팅을 위한 사용자 정의 ID를 추가하세요.", "bedrockModelsDescription": "Amazon Bedrock 모델은 AWS 지역별로 범위가 지정됩니다. /models에서 가져오거나 선택한 지역에서 활성화된 Bedrock 모델 ID를 추가하세요.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "간결한 CJK (文言)", "description": "한문 초간결 스타일 (중국어만 지원)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "쿨다운 대기", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "건강", "cliproxyapiPort": "포트", "qdrantHost": "호스트", - "qdrantCollection": "컬렉션", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "컬렉션" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "저장되었습니다.", "saveFailed": "저장할 수 없습니다.", "enableAria": "OmniGlyph 엔진 활성화", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "임베디드 서비스", @@ -9538,17 +9504,7 @@ "updatedShort": "업데이트됨", "lastRefreshed": "마지막 새로고침", "providerQuota": "제공자 할당량", - "providerQuotaHomeHint": "연결된 계정의 실시간 상태", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "연결된 계정의 실시간 상태" }, "modals": { "waitingAuth": "승인을 기다리는 중", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 63c9b916ae..d7d3d22e29 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -3718,11 +3718,7 @@ "errorDescription": "आम्ही सध्या कॉम्बो डेटा लोड करू शकत नाही. तुमचा कनेक्शन तपासा आणि पुन्हा प्रयत्न करा.", "errorId": "त्रुटी आयडी: {id}", "errorRetry": "पुन्हा प्रयत्न करा", - "comboLabel": "कॉम्बो", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "कॉम्बो" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "मोफत स्तर: 50 क्रेडिट्स/महिना (~25K–100K टोकन्स). ⚠️ Kiro ToS तृतीय-पक्ष प्रॉक्सी/हार्नेसच्या वापरावर बंदी घालते.", "codex": "सध्याच्या OAuth फ्लोसह OpenAI Codex कनेक्ट करा.", "qwen": "सध्याच्या OAuth फ्लोसह Qwen Code कनेक्ट करा.", - "github-models": "github.com/settings/tokens वर 'models: read' स्कोपसह GitHub PAT तयार करा", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens वर 'models: read' स्कोपसह GitHub PAT तयार करा" }, "passthroughModelsDescription": "{provider} प्रदाता-नेटिव्ह मॉडेल आयडी स्वीकारते. /मॉडेलमधून आयात करा किंवा राउटिंगसाठी सानुकूल आयडी जोडा.", "bedrockModelsDescription": "Amazon बेडरॉक मॉडेल्स AWS क्षेत्राद्वारे व्यापलेले आहेत. /मॉडेल्समधून आयात करा किंवा निवडलेल्या प्रदेशात सक्षम केलेले बेडरॉक मॉडेल आयडी जोडा.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "संक्षिप्त CJK (文言)", "description": "अभिजात-चिनी अति-संक्षिप्त शैली (केवळ चिनी भाषेसाठी उपलब्ध)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "कूलडाउनची प्रतीक्षा करा", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "आरोग्य", "cliproxyapiPort": "पोर्ट", "qdrantHost": "होस्ट", - "qdrantCollection": "संग्रह", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "संग्रह" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "जतन केले.", "saveFailed": "जतन करता आले नाही.", "enableAria": "OmniGlyph इंजिन सक्षम करा", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "एम्बेडेड सेवा", @@ -9538,17 +9504,7 @@ "updatedShort": "अपडेट केले", "lastRefreshed": "शेवटचे रिफ्रेश केलेले", "providerQuota": "प्रदाता कोटा", - "providerQuotaHomeHint": "कनेक्ट केलेल्या खात्यांमधील थेट स्थिती", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "कनेक्ट केलेल्या खात्यांमधील थेट स्थिती" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index b3799330b3..3f7482ff8f 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -3718,11 +3718,7 @@ "errorDescription": "Kami tidak dapat memuatkan data combo buat masa ini. Semak sambungan anda dan cuba lagi.", "errorId": "Ralat ID: {id}", "errorRetry": "Cuba Lagi", - "comboLabel": "Gabungan", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Gabungan" }, "costs": { "title": "Kos", @@ -6220,8 +6216,7 @@ "kiro": "Peringkat percuma: 50 kredit/bulan (~25K–100K token). ⚠️ ToS Kiro melarang penggunaan proksi/harness pihak ketiga.", "codex": "Sambungkan OpenAI Codex dengan aliran OAuth sedia ada.", "qwen": "Sambungkan Qwen Code dengan aliran OAuth sedia ada.", - "github-models": "Cipta PAT GitHub dengan skop 'models: read' di github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Cipta PAT GitHub dengan skop 'models: read' di github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} menerima ID model asli pembekal. Import daripada /models atau tambahkan ID tersuai untuk penghalaan.", "bedrockModelsDescription": "Model Amazon Bedrock diliputi oleh rantau AWS. Import daripada /models atau tambah ID model Bedrock yang didayakan di rantau yang dipilih.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Bahasa Cina Klasik (hanya tersedia untuk bahasa Cina)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Tunggu Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Kesihatan", "cliproxyapiPort": "Pelabuhan", "qdrantHost": "Hos", - "qdrantCollection": "Koleksi", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Koleksi" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Disimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Dayakan enjin OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Perkhidmatan Terbenam", @@ -9538,17 +9504,7 @@ "updatedShort": "Dikemas kini", "lastRefreshed": "Terakhir disegarkan", "providerQuota": "Kuota Penyedia", - "providerQuotaHomeHint": "Status langsung merentas akaun yang disambungkan", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Status langsung merentas akaun yang disambungkan" }, "modals": { "waitingAuth": "Menunggu Keizinan", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 2e967c5645..5355f29c17 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -3718,11 +3718,7 @@ "errorDescription": "We konden de combogegevens op dit moment niet laden. Controleer je verbinding en probeer het opnieuw.", "errorId": "Fout-ID: {id}", "errorRetry": "Probeer het opnieuw", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Kosten", @@ -6220,8 +6216,7 @@ "kiro": "Gratis tier: 50 credits/maand (~25K–100K tokens). ⚠️ Kiro ToS verbiedt het gebruik van externe proxy/harness.", "codex": "Verbind OpenAI Codex met de bestaande OAuth-flow.", "qwen": "Verbind Qwen Code met de bestaande OAuth-flow.", - "github-models": "Maak een GitHub PAT aan met de scope 'models: read' op github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Maak een GitHub PAT aan met de scope 'models: read' op github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} accepteert provider-native model-ID's. Importeer uit /models of voeg aangepaste ID's toe voor routering.", "bedrockModelsDescription": "Amazon Bedrock-modellen zijn afgestemd op de AWS-regio. Importeer uit /models of voeg Bedrock-model-ID's toe die zijn ingeschakeld in de geselecteerde regio.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Beknopt CJK (文言)", "description": "Klassiek-Chinese ultra-beknopte stijl (alleen beschikbaar voor Chinees)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Wacht op afkoelen", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Gezondheid", "cliproxyapiPort": "Haven", "qdrantHost": "Host", - "qdrantCollection": "Verzameling", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Verzameling" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Opgeslagen.", "saveFailed": "Opslaan mislukt.", "enableAria": "Schakel de OmniGlyph-engine in", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Ingebedde services", @@ -9538,17 +9504,7 @@ "updatedShort": "Bijgewerkt", "lastRefreshed": "Laatst vernieuwd", "providerQuota": "Providerquota", - "providerQuotaHomeHint": "Live status over verbonden accounts", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Live status over verbonden accounts" }, "modals": { "waitingAuth": "Wachten op toestemming", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 10b36bfe4a..77a2074b7e 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -3718,11 +3718,7 @@ "errorDescription": "Vi kunne ikke laste inn kombinasjonsdata akkurat nå. Sjekk tilkoblingen din og prøv igjen.", "errorId": "Feil-ID: {id}", "errorRetry": "Prøv igjen", - "comboLabel": "Kombinasjon", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinasjon" }, "costs": { "title": "Kostnader", @@ -6220,8 +6216,7 @@ "kiro": "Gratisnivå: 50 kreditter/måned (~25K–100K tokens). ⚠️ Kiros brukervilkår forbyr bruk av tredjeparts proxy/harness.", "codex": "Koble til OpenAI Codex med den eksisterende OAuth-flyten.", "qwen": "Koble til Qwen Code med den eksisterende OAuth-flyten.", - "github-models": "Opprett et GitHub PAT med 'models: read'-omfang på github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Opprett et GitHub PAT med 'models: read'-omfang på github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} godtar leverandør-native modell-ID-er. Importer fra /models eller legg til egendefinerte ID-er for ruting.", "bedrockModelsDescription": "Amazon Bedrock-modeller er omfattet av AWS-regionen. Importer fra /models eller legg til berggrunnsmodell-IDer aktivert i den valgte regionen.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Kortfattet CJK (文言)", "description": "Klassisk-kinesisk ultrakortfattet stil (kun tilgjengelig for kinesisk)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Vent på nedkjøling", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Helse", "cliproxyapiPort": "Port", "qdrantHost": "Vert", - "qdrantCollection": "Samling", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Samling" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Lagret.", "saveFailed": "Kunne ikke lagre.", "enableAria": "Aktiver OmniGlyph-motoren", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Innebygde tjenester", @@ -9538,17 +9504,7 @@ "updatedShort": "Oppdatert", "lastRefreshed": "Sist oppdatert", "providerQuota": "Leverandørkvote", - "providerQuotaHomeHint": "Sanntidsstatus på tvers av tilkoblede kontoer", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Sanntidsstatus på tvers av tilkoblede kontoer" }, "modals": { "waitingAuth": "Venter på autorisasjon", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 09b239e750..cfe0a6e006 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -3718,11 +3718,7 @@ "errorDescription": "Hindi namin ma-load ang combo data sa ngayon. Suriin ang iyong koneksyon at subukan muli.", "errorId": "Error ID: {id}", "errorRetry": "Subukan Muli", - "comboLabel": "Kumbinasyon", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kumbinasyon" }, "costs": { "title": "Mga gastos", @@ -6220,8 +6216,7 @@ "kiro": "Libreng tier: 50 credits/buwan (~25K–100K tokens). ⚠️ Ipinagbabawal ng Kiro ToS ang paggamit ng third-party proxy/harness.", "codex": "Ikonekta ang OpenAI Codex gamit ang umiiral na OAuth flow.", "qwen": "Ikonekta ang Qwen Code gamit ang umiiral na OAuth flow.", - "github-models": "Gumawa ng GitHub PAT na may 'models: read' na scope sa github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Gumawa ng GitHub PAT na may 'models: read' na scope sa github.com/settings/tokens" }, "passthroughModelsDescription": "Tumatanggap ang {provider} ng mga provider-native na model ID. Mag-import mula sa /models o magdagdag ng mga custom na ID para sa pagruruta.", "bedrockModelsDescription": "Ang mga modelo ng Amazon Bedrock ay saklaw ng rehiyon ng AWS. Mag-import mula sa /models o magdagdag ng mga Bedrock model ID na pinagana sa napiling rehiyon.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Maikling CJK (文言)", "description": "Klasikong Tsino na ultra-maikling estilo (magagamit lamang para sa Tsino)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Maghintay para sa Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Kalusugan", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Koleksyon", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Koleksyon" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Nai-save.", "saveFailed": "Hindi mai-save.", "enableAria": "I-enable ang OmniGlyph engine", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Mga Naka-embed na Serbisyo", @@ -9538,17 +9504,7 @@ "updatedShort": "Na-update", "lastRefreshed": "Huling na-refresh", "providerQuota": "Quota ng Provider", - "providerQuotaHomeHint": "Live na status sa lahat ng nakakonektang account", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Live na status sa lahat ng nakakonektang account" }, "modals": { "waitingAuth": "Naghihintay ng Awtorisasyon", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 30a734c1dd..04590ea8ad 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -3718,11 +3718,7 @@ "errorDescription": "Nie mogliśmy teraz załadować danych combo. Sprawdź swoje połączenie i spróbuj ponownie.", "errorId": "Identyfikator błędu: {id}", "errorRetry": "Spróbuj ponownie", - "comboLabel": "Kombinacja", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinacja" }, "costs": { "title": "Koszty", @@ -6220,8 +6216,7 @@ "kiro": "Darmowy plan: 50 kredytów/miesiąc (~25K–100K tokenów). ⚠️ ToS Kiro zabrania korzystania z zewnętrznych proxy/harness.", "codex": "Połącz OpenAI Codex za pomocą istniejącego przepływu OAuth.", "qwen": "Połącz Qwen Code za pomocą istniejącego przepływu OAuth.", - "github-models": "Utwórz token GitHub PAT z zakresem 'models: read' na github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Utwórz token GitHub PAT z zakresem 'models: read' na github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} akceptuje natywne ID models dla provider. Zaimportuj z /models lub dodaj własne ID do routing.", "bedrockModelsDescription": "Models Amazon Bedrock są ograniczone do regionu AWS. Zaimportuj z /models lub dodaj Bedrock ID models włączone w wybranym regionie.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Zwięzłe CJK (文言)", "description": "Klasyczny chiński styl ultra-zwięzły (dostępny tylko dla języka chińskiego)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Czekaj na cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Zdrowie", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Kolekcja", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Kolekcja" }, "contextRtk": { "title": "Silnik RTK", @@ -8637,28 +8624,7 @@ "saved": "Zapisano.", "saveFailed": "Nie można zapisać.", "enableAria": "Włącz silnik OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Usługi wbudowane", @@ -9538,17 +9504,7 @@ "updatedShort": "Zaktualizowano", "lastRefreshed": "Ostatnio odświeżono", "providerQuota": "Provider Quota", - "providerQuotaHomeHint": "Status na żywo na połączonych kontach", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Status na żywo na połączonych kontach" }, "modals": { "waitingAuth": "Oczekiwanie na autoryzację", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 00cdc772d9..a4f8a5f3cf 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -3718,11 +3718,7 @@ "errorDescription": "Não conseguimos carregar os dados do combo no momento. Verifique sua conexão e tente novamente.", "errorId": "ID de Erro: {id}", "errorRetry": "Tente Novamente", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Custos", @@ -6220,8 +6216,7 @@ "kiro": "Nível gratuito: 50 créditos/mês (~25K–100K tokens). ⚠️ Os Termos de Serviço do Kiro proíbem o uso de proxy/harness de terceiros.", "codex": "Conecte o OpenAI Codex com o fluxo OAuth existente.", "qwen": "Conecte o Qwen Code com o fluxo OAuth existente.", - "github-models": "Crie um PAT do GitHub com o escopo 'models: read' em github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Crie um PAT do GitHub com o escopo 'models: read' em github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} aceita IDs de modelo nativos do provedor. Importe de /models ou adicione IDs personalizados para roteamento.", "bedrockModelsDescription": "Os modelos Amazon Bedrock têm escopo definido por região da AWS. Importe de /models ou adicione IDs de modelo Bedrock habilitados na região selecionada.", @@ -7215,7 +7210,6 @@ "configured": "configurado", "none": "Nenhum", "modelOverrideValuePlaceholder": "Valor numérico", - "modelOverrideReasoningEffortsPlaceholder": "Lista em inglês separada por vírgulas, ex. low, medium, high", "addKeyValue": "Adicionar valor da chave", "noModelOverrides": "Nenhuma substituição configurada para este modelo.", "modelOverrideLoadFailed": "Falha ao carregar substituições de modelo", @@ -7790,10 +7784,6 @@ "terse-cjk": { "label": "CJK conciso (文言)", "description": "Estilo ultra-conciso em chinês clássico (disponível apenas para chinês)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Aguarde o resfriamento", @@ -8226,11 +8216,7 @@ "cliproxyapiHealth": "Saúde", "cliproxyapiPort": "Porta", "qdrantHost": "Host", - "qdrantCollection": "Coleção", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Coleção" }, "contextRtk": { "title": "Motor RTK", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index cb45bacae5..9935bfe7ef 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -3718,11 +3718,7 @@ "errorDescription": "Não conseguimos carregar os dados do combo neste momento. Verifique a sua ligação e tente novamente.", "errorId": "ID de Erro: {id}", "errorRetry": "Tente Novamente", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Custos", @@ -6220,8 +6216,7 @@ "kiro": "Nível gratuito: 50 créditos/mês (~25K–100K tokens). ⚠️ Os ToS do Kiro proíbem a utilização de proxy/harness de terceiros.", "codex": "Ligar o OpenAI Codex com o fluxo OAuth existente.", "qwen": "Ligar o Qwen Code com o fluxo OAuth existente.", - "github-models": "Crie um PAT do GitHub com o âmbito 'models: read' em github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Crie um PAT do GitHub com o âmbito 'models: read' em github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} aceita IDs de modelo nativos do provedor. Importe de /models ou adicione IDs personalizados para roteamento.", "bedrockModelsDescription": "Os modelos Amazon Bedrock têm escopo definido por região da AWS. Importe de /models ou adicione IDs de modelo Bedrock habilitados na região selecionada.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK conciso (文言)", "description": "Estilo ultraconciso em chinês clássico (disponível apenas para chinês)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Aguarde o resfriamento", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Saúde", "cliproxyapiPort": "Porto", "qdrantHost": "Anfitrião", - "qdrantCollection": "Coleção", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Coleção" }, "contextRtk": { "title": "Motor RTK", @@ -8637,28 +8624,7 @@ "saved": "Guardado.", "saveFailed": "Não foi possível guardar.", "enableAria": "Ativar o motor OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Serviços Incorporados", @@ -9538,17 +9504,7 @@ "updatedShort": "Atualizado", "lastRefreshed": "Última atualização", "providerQuota": "Quota do fornecedor", - "providerQuotaHomeHint": "Estado em tempo real em todas as contas associadas", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Estado em tempo real em todas as contas associadas" }, "modals": { "waitingAuth": "Aguardando autorização", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 11f9dcdb85..cec84fa369 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -3718,11 +3718,7 @@ "errorDescription": "Nu am putut încărca datele combo în acest moment. Verifică-ți conexiunea și încearcă din nou.", "errorId": "ID eroare: {id}", "errorRetry": "Încearcă din nou", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Costuri", @@ -6220,8 +6216,7 @@ "kiro": "Nivel gratuit: 50 de credite/lună (~25K–100K tokenuri). ⚠️ Kiro ToS interzice utilizarea de proxy/harness terțe.", "codex": "Conectați OpenAI Codex cu fluxul OAuth existent.", "qwen": "Conectați Qwen Code cu fluxul OAuth existent.", - "github-models": "Creați un GitHub PAT cu scope-ul 'models: read' la github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Creați un GitHub PAT cu scope-ul 'models: read' la github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} acceptă ID-uri de model native ale furnizorului. Importați din /modele sau adăugați ID-uri personalizate pentru rutare.", "bedrockModelsDescription": "Modelele Amazon Bedrock sunt acoperite de regiunea AWS. Importați din /modele sau adăugați ID-uri de model Bedrock activate în regiunea selectată.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK concis (文言)", "description": "Stil ultra-concis în chineza clasică (disponibil doar pentru chineză)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Așteptați răcirea", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Sănătate", "cliproxyapiPort": "Port", "qdrantHost": "Gazdă", - "qdrantCollection": "Colecție", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Colecție" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Salvat.", "saveFailed": "Nu s-a putut salva.", "enableAria": "Activează motorul OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Servicii integrate", @@ -9538,17 +9504,7 @@ "updatedShort": "Actualizat", "lastRefreshed": "Ultima reîmprospătare", "providerQuota": "Cotă furnizor", - "providerQuotaHomeHint": "Stare în timp real pentru conturile conectate", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Stare în timp real pentru conturile conectate" }, "modals": { "waitingAuth": "În așteptarea autorizației", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 108c4ce690..ba9a4e812e 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -3718,11 +3718,7 @@ "errorDescription": "Мы не смогли загрузить данные комбо в данный момент. Проверьте ваше соединение и попробуйте снова.", "errorId": "Идентификатор ошибки: {id}", "errorRetry": "Попробуйте снова", - "comboLabel": "Комбо", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Комбо" }, "costs": { "title": "Затраты", @@ -6220,8 +6216,7 @@ "kiro": "Бесплатный тариф: 50 кредитов/месяц (~25K–100K токенов). ⚠️ Условия использования Kiro запрещают использование сторонних прокси/оболочек.", "codex": "Подключите OpenAI Codex с помощью существующего процесса OAuth.", "qwen": "Подключите Qwen Code с помощью существующего процесса OAuth.", - "github-models": "Создайте GitHub PAT с областью доступа 'models: read' на github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Создайте GitHub PAT с областью доступа 'models: read' на github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} принимает собственные идентификаторы моделей поставщика. Импортируйте из /models или добавляйте собственные идентификаторы для маршрутизации.", "bedrockModelsDescription": "Модели Amazon Bedrock охватываются регионом AWS. Импортируйте из /models или добавьте идентификаторы моделей Bedrock, включенные в выбранном регионе.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Краткий CJK (文言)", "description": "Ультракраткий классический китайский стиль (доступно только для китайского языка)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Подождите перезарядки", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Здоровье", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Коллекция", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Коллекция" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Сохранено.", "saveFailed": "Не удалось сохранить.", "enableAria": "Включить движок OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Встроенные службы", @@ -9538,17 +9504,7 @@ "updatedShort": "Обновлено", "lastRefreshed": "Последнее обновление", "providerQuota": "Квота Провайдера", - "providerQuotaHomeHint": "Живой статус по подключенным аккаунтам", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Живой статус по подключенным аккаунтам" }, "modals": { "waitingAuth": "Ожидание авторизации", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 2b1f8c38c8..fbd89bef06 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -3718,11 +3718,7 @@ "errorDescription": "Momentálne sa nám nepodarilo načítať údaje kombinácie. Skontrolujte svoje pripojenie a skúste to znova.", "errorId": "Chyba ID: {id}", "errorRetry": "Skúste znova", - "comboLabel": "Kombinácia", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinácia" }, "costs": { "title": "náklady", @@ -6220,8 +6216,7 @@ "kiro": "Bezplatná úroveň: 50 kreditov/mesiac (~25k – 100k tokenov). ⚠️ Podmienky používania (ToS) Kiro zakazujú používanie proxy/harness tretích strán.", "codex": "Pripojte OpenAI Codex pomocou existujúceho toku OAuth.", "qwen": "Pripojte Qwen Code pomocou existujúceho toku OAuth.", - "github-models": "Vytvorte si GitHub PAT s rozsahom 'models: read' na adrese github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Vytvorte si GitHub PAT s rozsahom 'models: read' na adrese github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} akceptuje ID modelu natívneho poskytovateľa. Importujte z /models alebo pridajte vlastné ID pre smerovanie.", "bedrockModelsDescription": "Modely Amazon Bedrock sú vymedzené podľa regiónu AWS. Importovať z /models alebo pridať ID modelu Bedrock povolené vo vybranom regióne.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Stručné CJK (文言)", "description": "Klasický čínsky ultra stručný štýl (dostupný len pre čínštinu)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Počkajte na Cooldown", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Zdravie", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Zbierka", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Zbierka" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Uložené.", "saveFailed": "Nepodarilo sa uložiť.", "enableAria": "Povoliť engine OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Vstavané služby", @@ -9538,17 +9504,7 @@ "updatedShort": "Aktualizované", "lastRefreshed": "Naposledy obnovené", "providerQuota": "Kvóta poskytovateľa", - "providerQuotaHomeHint": "Aktuálny stav naprieč pripojenými účtami", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Aktuálny stav naprieč pripojenými účtami" }, "modals": { "waitingAuth": "Čaká sa na autorizáciu", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 0aa1969eda..75b165386a 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -3718,11 +3718,7 @@ "errorDescription": "Vi kunde inte ladda combo-data just nu. Kontrollera din anslutning och försök igen.", "errorId": "Fel-ID: {id}", "errorRetry": "Försök igen", - "comboLabel": "Kombination", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombination" }, "costs": { "title": "Kostnader", @@ -6220,8 +6216,7 @@ "kiro": "Gratisnivå: 50 krediter/månad (~25K–100K tokens). ⚠️ Kiros användarvillkor förbjuder användning av tredjepartsproxy/harness.", "codex": "Anslut OpenAI Codex med det befintliga OAuth-flödet.", "qwen": "Anslut Qwen Code med det befintliga OAuth-flödet.", - "github-models": "Skapa en GitHub PAT med omfånget 'models: read' på github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Skapa en GitHub PAT med omfånget 'models: read' på github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} accepterar leverantörsbaserade modell-ID:n. Importera från /models eller lägg till anpassade ID:n för routing.", "bedrockModelsDescription": "Amazon Bedrock-modeller omfattas av AWS-regionen. Importera från /models eller lägg till Bedrock-modell-ID:n aktiverade i den valda regionen.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Kortfattad CJK (文言)", "description": "Klassisk kinesisk ultrakortfattad stil (endast tillgänglig för kinesiska)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Vänta på nedkylning", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Hälsa", "cliproxyapiPort": "Port", "qdrantHost": "Värd", - "qdrantCollection": "Samling", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Samling" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Sparat.", "saveFailed": "Kunde inte spara.", "enableAria": "Aktivera OmniGlyph-motorn", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Inbäddade tjänster", @@ -9538,17 +9504,7 @@ "updatedShort": "Uppdaterad", "lastRefreshed": "Senast uppdaterad", "providerQuota": "Leverantörskvot", - "providerQuotaHomeHint": "Livestatus för anslutna konton", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Livestatus för anslutna konton" }, "modals": { "waitingAuth": "Väntar på auktorisering", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 77ae0f4373..15f8f0e290 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -3718,11 +3718,7 @@ "errorDescription": "Hatuwezi kupakia data ya combo kwa sasa. Angalia muunganisho wako na ujaribu tena.", "errorId": "Kosa ID: {id}", "errorRetry": "Jaribu Tena", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "Kiwango cha bure: mikopo 50/mwezi (~tokeni 25K–100K). ⚠️ Kiro ToS inakataza matumizi ya proksi/harness ya wahusika wengine.", "codex": "Unganisha OpenAI Codex na mtiririko uliopo wa OAuth.", "qwen": "Unganisha Qwen Code na mtiririko uliopo wa OAuth.", - "github-models": "Unda PAT ya GitHub yenye upeo wa 'models: read' kwenye github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Unda PAT ya GitHub yenye upeo wa 'models: read' kwenye github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} inakubali vitambulisho vya asili vya mtoa huduma. Ingiza kutoka /miundo au ongeza vitambulisho maalum vya kuelekeza.", "bedrockModelsDescription": "Mitindo ya Amazon Bedrock inatolewa na eneo la AWS. Ingiza kutoka /miundo au ongeza vitambulisho vya muundo wa Bedrock vilivyowezeshwa katika eneo lililochaguliwa.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK Fupi (文言)", "description": "Mtindo mfupi zaidi wa Kichina cha Kale (inapatikana kwa Kichina pekee)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Subiri hadi Kupunguza joto", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Afya", "cliproxyapiPort": "Bandari", "qdrantHost": "Mwenyeji", - "qdrantCollection": "Mkusanyiko", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Mkusanyiko" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Imehifadhiwa.", "saveFailed": "Imeshindwa kuhifadhi.", "enableAria": "Wezesha injini ya OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Huduma Zilizopachikwa", @@ -9538,17 +9504,7 @@ "updatedShort": "Imesasishwa", "lastRefreshed": "Ilihuishwa mara ya mwisho", "providerQuota": "Kiwango cha Mtoa Huduma", - "providerQuotaHomeHint": "Hali ya moja kwa moja kwenye akaunti zilizounganishwa", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Hali ya moja kwa moja kwenye akaunti zilizounganishwa" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index d944a2e6e4..d9bb96f0f4 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -3718,11 +3718,7 @@ "errorDescription": "நாங்கள் தற்போது கம்போ தரவுகளை ஏற்ற முடியவில்லை. உங்கள் இணைப்பை சரிபார்க்கவும் மற்றும் மீண்டும் முயற்சிக்கவும்.", "errorId": "பிழை அடையாளம்: {id}", "errorRetry": "மீண்டும் முயற்சி செய்", - "comboLabel": "கொம்போ", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "கொம்போ" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "இலவச அடுக்கு: 50 கிரெடிட்கள்/மாதம் (~25K–100K டோக்கன்கள்). ⚠️ Kiro ToS மூன்றாம் தரப்பு proxy/harness பயன்பாட்டைத் தடைசெய்கிறது.", "codex": "தற்போதுள்ள OAuth செயல்முறையுடன் OpenAI Codex-ஐ இணைக்கவும்.", "qwen": "தற்போதுள்ள OAuth செயல்முறையுடன் Qwen Code-ஐ இணைக்கவும்.", - "github-models": "github.com/settings/tokens இல் 'models: read' வரம்புடன் ஒரு GitHub PAT ஐ உருவாக்கவும்", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens இல் 'models: read' வரம்புடன் ஒரு GitHub PAT ஐ உருவாக்கவும்" }, "passthroughModelsDescription": "{provider} வழங்குநரின் சொந்த மாதிரி ஐடிகளை ஏற்றுக்கொள்கிறது. /மாடல்களில் இருந்து இறக்குமதி செய்யவும் அல்லது ரூட்டிங் செய்ய தனிப்பயன் ஐடிகளைச் சேர்க்கவும்.", "bedrockModelsDescription": "அமேசான் பெட்ராக் மாதிரிகள் AWS பிராந்தியத்தால் ஸ்கோப் செய்யப்படுகின்றன. /மாடல்களில் இருந்து இறக்குமதி செய்யவும் அல்லது தேர்ந்தெடுக்கப்பட்ட பகுதியில் செயல்படுத்தப்பட்ட பெட்ராக் மாடல் ஐடிகளைச் சேர்க்கவும்.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "சுருக்கமான CJK (文言)", "description": "செம்மொழி-சீன மிகச் சுருக்கமான நடை (சீன மொழிக்கு மட்டுமே கிடைக்கும்)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "கூல்டவுனுக்காக காத்திருங்கள்", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "ஆரோக்கியம்", "cliproxyapiPort": "போர்ட்", "qdrantHost": "விருந்தினர்", - "qdrantCollection": "கலெக்ஷன்", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "கலெக்ஷன்" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "சேமிக்கப்பட்டது.", "saveFailed": "சேமிக்க முடியவில்லை.", "enableAria": "OmniGlyph இயந்திரத்தை இயக்கு", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "உட்பொதிக்கப்பட்ட சேவைகள்", @@ -9538,17 +9504,7 @@ "updatedShort": "புதுப்பிக்கப்பட்டது", "lastRefreshed": "கடைசியாகப் புதுப்பிக்கப்பட்டது", "providerQuota": "வழங்குநர் ஒதுக்கீடு", - "providerQuotaHomeHint": "இணைக்கப்பட்ட கணக்குகளின் நேரலை நிலை", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "இணைக்கப்பட்ட கணக்குகளின் நேரலை நிலை" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index ac7538ee06..65aa08d437 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -3718,11 +3718,7 @@ "errorDescription": "మేము ప్రస్తుతం కాంబో డేటాను లోడ్ చేయలేకపోయాము. మీ కనెక్షన్‌ను తనిఖీ చేసి మళ్లీ ప్రయత్నించండి.", "errorId": "లోపం ID: {id}", "errorRetry": "మరలా ప్రయత్నించండి", - "comboLabel": "కాంబో", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "కాంబో" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "ఉచిత శ్రేణి: 50 క్రెడిట్‌లు/నెల (~25K–100K టోకెన్‌లు). ⚠️ Kiro ToS థర్డ్-పార్టీ ప్రాక్సీ/హార్నెస్ వినియోగాన్ని నిషేధిస్తుంది.", "codex": "ప్రస్తుత OAuth flowతో OpenAI Codexని కనెక్ట్ చేయండి.", "qwen": "ప్రస్తుత OAuth flowతో Qwen Codeని కనెక్ట్ చేయండి.", - "github-models": "github.com/settings/tokens వద్ద 'models: read' స్కోప్‌తో GitHub PATని సృష్టించండి", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens వద్ద 'models: read' స్కోప్‌తో GitHub PATని సృష్టించండి" }, "passthroughModelsDescription": "{provider} ప్రొవైడర్-స్థానిక మోడల్ IDలను అంగీకరిస్తుంది. /మోడల్స్ నుండి దిగుమతి చేయండి లేదా రూటింగ్ కోసం అనుకూల IDలను జోడించండి.", "bedrockModelsDescription": "అమెజాన్ బెడ్‌రాక్ మోడల్‌లు AWS ప్రాంతం ద్వారా స్కోప్ చేయబడ్డాయి. /మోడల్స్ నుండి దిగుమతి చేయండి లేదా ఎంచుకున్న ప్రాంతంలో ప్రారంభించబడిన బెడ్‌రాక్ మోడల్ IDలను జోడించండి.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "సంక్షిప్త CJK (文言)", "description": "క్లాసికల్-చైనీస్ అల్ట్రా-సంక్షిప్త శైలి (చైనీస్ కోసం మాత్రమే అందుబాటులో ఉంది)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "కూల్‌డౌన్ కోసం వేచి ఉండండి", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "ఆరోగ్యం", "cliproxyapiPort": "పోర్ట్", "qdrantHost": "హోస్ట్", - "qdrantCollection": "సేకరణ", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "సేకరణ" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "సేవ్ చేయబడింది.", "saveFailed": "సేవ్ చేయడం సాధ్యపడలేదు.", "enableAria": "OmniGlyph ఇంజిన్‌ను ఎనేబుల్ చేయండి", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "ఎంబెడెడ్ సేవలు", @@ -9538,17 +9504,7 @@ "updatedShort": "అప్‌డేట్ చేయబడింది", "lastRefreshed": "చివరిగా రిఫ్రెష్ చేయబడింది", "providerQuota": "ప్రొవైడర్ కోటా", - "providerQuotaHomeHint": "కనెక్ట్ చేయబడిన ఖాతాల ప్రత్యక్ష స్థితి", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "కనెక్ట్ చేయబడిన ఖాతాల ప్రత్యక్ష స్థితి" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index c7bc2b9524..ed1e5d4063 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -3718,11 +3718,7 @@ "errorDescription": "ไม่สามารถโหลดข้อมูลคอมโบได้ในขณะนี้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองอีกครั้ง.", "errorId": "รหัสข้อผิดพลาด: {id}", "errorRetry": "ลองอีกครั้ง", - "comboLabel": "คอมโบ", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "คอมโบ" }, "costs": { "title": "ค่าใช้จ่าย", @@ -6220,8 +6216,7 @@ "kiro": "ระดับฟรี: 50 เครดิต/เดือน (~25K–100K โทเค็น) ⚠️ ToS ของ Kiro ห้ามใช้พร็อกซี/harness ของบุคคลที่สาม", "codex": "เชื่อมต่อ OpenAI Codex ด้วยโฟลว์ OAuth ที่มีอยู่", "qwen": "เชื่อมต่อ Qwen Code ด้วยโฟลว์ OAuth ที่มีอยู่", - "github-models": "สร้าง GitHub PAT ที่มีขอบเขต 'models: read' ที่ github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "สร้าง GitHub PAT ที่มีขอบเขต 'models: read' ที่ github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} ยอมรับ ID โมเดลดั้งเดิมของผู้ให้บริการ นำเข้าจาก /models หรือเพิ่ม ID ที่กำหนดเองสำหรับการกำหนดเส้นทาง", "bedrockModelsDescription": "โมเดล Amazon Bedrock มีการกำหนดขอบเขตตามภูมิภาค AWS นำเข้าจาก /models หรือเพิ่มรหัสรุ่น Bedrock ที่เปิดใช้งานในภูมิภาคที่เลือก", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "CJK แบบกระชับ (文言)", "description": "สไตล์ภาษาจีนคลาสสิกแบบกระชับอย่างยิ่ง (ใช้ได้เฉพาะภาษาจีนเท่านั้น)" - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "รอคูลดาวน์", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "สุขภาพ", "cliproxyapiPort": "พอร์ต", "qdrantHost": "โฮสต์", - "qdrantCollection": "การรวบรวม", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "การรวบรวม" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "บันทึกแล้ว", "saveFailed": "ไม่สามารถบันทึกได้", "enableAria": "เปิดใช้งานเอนจิน OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "บริการแบบฝัง", @@ -9538,17 +9504,7 @@ "updatedShort": "อัปเดตแล้ว", "lastRefreshed": "รีเฟรชล่าสุดเมื่อ", "providerQuota": "โควตาผู้ให้บริการ", - "providerQuotaHomeHint": "สถานะแบบเรียลไทม์ของบัญชีที่เชื่อมต่อทั้งหมด", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "สถานะแบบเรียลไทม์ของบัญชีที่เชื่อมต่อทั้งหมด" }, "modals": { "waitingAuth": "รอการอนุญาต", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 96ff186627..5ef5816345 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -3718,11 +3718,7 @@ "errorDescription": "Şu anda kombinasyon verilerini yükleyemedik. Bağlantınızı kontrol edin ve tekrar deneyin.", "errorId": "Hata Kimliği: {id}", "errorRetry": "Tekrar Dene", - "comboLabel": "Kombinasyon", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Kombinasyon" }, "costs": { "title": "Maliyetler", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Mevcut OAuth akışı ile OpenAI Codex'i bağlayın.", "qwen": "Mevcut OAuth akışı ile Qwen Code'u bağlayın.", - "github-models": "github.com/settings/tokens adresinde 'models: read' kapsamına sahip bir GitHub PAT oluşturun", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens adresinde 'models: read' kapsamına sahip bir GitHub PAT oluşturun" }, "passthroughModelsDescription": "{provider} sağlayıcıya özgü model kimliklerini kabul eder. /models'den içe aktarın veya yönlendirme için özel kimlikler ekleyin.", "bedrockModelsDescription": "Amazon Bedrock modelleri AWS bölgesi kapsamındadır. /models'den içe aktarın veya seçilen bölgede etkin olan Bedrock model kimliklerini ekleyin.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Kısa ve öz CJK (文言)", "description": "Klasik Çince ultra kısa ve öz stil (yalnızca Çince için kullanılabilir)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Bekleme Süresini Bekleyin", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Sağlık", "cliproxyapiPort": "Port", "qdrantHost": "Ana Bilgisayar", - "qdrantCollection": "Koleksiyon", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Koleksiyon" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "Kaydedildi.", "saveFailed": "Kaydedilemedi.", "enableAria": "OmniGlyph motorunu etkinleştir", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Gömülü Servisler", @@ -9538,17 +9504,7 @@ "updatedShort": "Güncellendi", "lastRefreshed": "Son yenileme", "providerQuota": "Sağlayıcı Kotası", - "providerQuotaHomeHint": "Bağlı hesaplar genelinde canlı durum", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Bağlı hesaplar genelinde canlı durum" }, "modals": { "waitingAuth": "Yetkilendirme bekleniyor", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 2194b88f3d..9fe5efbd52 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -3718,11 +3718,7 @@ "errorDescription": "Ми не змогли завантажити дані комбо прямо зараз. Перевірте своє з'єднання та спробуйте ще раз.", "errorId": "Ідентифікатор помилки: {id}", "errorRetry": "Спробуйте ще раз", - "comboLabel": "Комбо", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Комбо" }, "costs": { "title": "Витрати", @@ -6220,8 +6216,7 @@ "kiro": "Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use.", "codex": "Connect OpenAI Codex with the existing OAuth flow.", "qwen": "Connect Qwen Code with the existing OAuth flow.", - "github-models": "Створіть GitHub PAT з областю видимості 'models: read' на github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Створіть GitHub PAT з областю видимості 'models: read' на github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} приймає власні ідентифікатори моделі постачальника. Імпортуйте з /models або додайте власні ідентифікатори для маршрутизації.", "bedrockModelsDescription": "Моделі Amazon Bedrock залежать від регіону AWS. Імпортуйте з /models або додайте ідентифікатори моделі Bedrock, активовані у вибраному регіоні.", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "Стисла CJK (文言)", "description": "Класичний китайський ультрастислий стиль (доступно тільки для китайської)." - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "Дочекайтеся перезарядки", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "Здоров'я", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Колекція", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "Колекція" }, "contextRtk": { "title": "Двигун RTK", @@ -8637,28 +8624,7 @@ "saved": "Збережено.", "saveFailed": "Не вдалося зберегти.", "enableAria": "Увімкнути рушій OmniGlyph", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "Вбудовані служби", @@ -9538,17 +9504,7 @@ "updatedShort": "Оновлено", "lastRefreshed": "Останнє оновлення", "providerQuota": "Квота провайдера", - "providerQuotaHomeHint": "Стан у реальному часі за підключеними акаунтами", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "Стан у реальному часі за підключеними акаунтами" }, "modals": { "waitingAuth": "Очікування авторизації", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 2fab849e16..22deb83299 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -3718,11 +3718,7 @@ "errorDescription": "ہم اس وقت کومبو ڈیٹا لوڈ نہیں کر سکے۔ اپنی کنکشن چیک کریں اور دوبارہ کوشش کریں۔", "errorId": "خرابی کی شناخت: {id}", "errorRetry": "پھر کوشش کریں", - "comboLabel": "کمبو", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "کمبو" }, "costs": { "title": "Costs", @@ -6220,8 +6216,7 @@ "kiro": "مفت پلان: 50 کریڈٹس/مہینہ (~25K–100K ٹوکنز)۔ ⚠️ Kiro ToS فریقِ ثالث کے پراکسی/ہارنس کے استعمال سے منع کرتا ہے۔", "codex": "OpenAI Codex کو موجودہ OAuth فلو سے منسلک کریں۔", "qwen": "Qwen Code کو موجودہ OAuth فلو سے منسلک کریں۔", - "github-models": "github.com/settings/tokens پر 'models: read' اسکوپ کے ساتھ ایک GitHub PAT بنائیں", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "github.com/settings/tokens پر 'models: read' اسکوپ کے ساتھ ایک GitHub PAT بنائیں" }, "passthroughModelsDescription": "{provider} فراہم کنندہ کے مقامی ماڈل IDs کو قبول کرتا ہے۔ /ماڈلز سے درآمد کریں یا روٹنگ کے لیے حسب ضرورت IDs شامل کریں۔", "bedrockModelsDescription": "ایمیزون بیڈرک ماڈلز کا دائرہ AWS ریجن کے ذریعہ کیا گیا ہے۔ /ماڈلز سے درآمد کریں یا منتخب علاقے میں فعال کردہ Bedrock ماڈل IDs شامل کریں۔", @@ -7789,10 +7784,6 @@ "terse-cjk": { "label": "مختصر CJK (文言)", "description": "کلاسیکی چینی انتہائی مختصر انداز (صرف چینی زبان کے لیے دستیاب ہے)۔" - }, - "i-have-adhd": { - "label": "I have ADHD (action-first)", - "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." } }, "resilienceWaitForCooldown": "کولڈاؤن کا انتظار کریں۔", @@ -8225,11 +8216,7 @@ "cliproxyapiHealth": "صحت", "cliproxyapiPort": "پورٹ", "qdrantHost": "میزبان", - "qdrantCollection": "اجتماع", - "reasoningTokenBuffer": "Reasoning token buffer", - "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", - "zeroLatencyOptimizations": "Zero-latency optimizations", - "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests." + "qdrantCollection": "اجتماع" }, "contextRtk": { "title": "RTK Engine", @@ -8637,28 +8624,7 @@ "saved": "محفوظ ہو گیا۔", "saveFailed": "محفوظ نہیں ہو سکا۔", "enableAria": "OmniGlyph انجن فعال کریں", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "ایمبیڈڈ سروسز", @@ -9538,17 +9504,7 @@ "updatedShort": "اپ ڈیٹ شدہ", "lastRefreshed": "آخری بار ریفریش کیا گیا", "providerQuota": "فراہم کنندہ کا کوٹہ", - "providerQuotaHomeHint": "منسلک اکاؤنٹس میں لائیو اسٹیٹس", - "kimiExtraUsageCredits": "Extra Usage Credits", - "kimiExtraUsage": "Extra Usage", - "kimiExtraUsageEnabled": "Enabled", - "kimiExtraUsageDisabled": "Disabled", - "kimiExtraUsageFrozen": "Frozen", - "kimiExtraUsageUnavailable": "Unavailable", - "kimiMonthlyUsed": "Used this month", - "kimiMonthlyLimit": "Monthly limit", - "kimiMonthlyLimitUnlimited": "Unlimited", - "kimiAdditionalCredits": "Additional Credits" + "providerQuotaHomeHint": "منسلک اکاؤنٹس میں لائیو اسٹیٹس" }, "modals": { "waitingAuth": "Waiting for Authorization", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 3953260e5c..eb1cb41b6e 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -3718,11 +3718,7 @@ "errorDescription": "Hiện không thể tải dữ liệu combo. Hãy kiểm tra kết nối rồi thử lại.", "errorId": "ID lỗi: {id}", "errorRetry": "Thử lại", - "comboLabel": "Combo", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "Combo" }, "costs": { "title": "Chi phí", @@ -6220,8 +6216,7 @@ "kiro": "Gói miễn phí: 50 tín dụng/tháng (khoảng 25–100 nghìn token). ⚠️ Điều khoản Kiro cấm sử dụng proxy/harness của bên thứ ba.", "codex": "Kết nối OpenAI Codex bằng luồng OAuth hiện có.", "qwen": "Kết nối Qwen Code bằng luồng OAuth hiện có.", - "github-models": "Tạo GitHub PAT với phạm vi 'models: read' tại github.com/settings/tokens", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "Tạo GitHub PAT với phạm vi 'models: read' tại github.com/settings/tokens" }, "passthroughModelsDescription": "{provider} chấp nhận ID mô hình gốc của nhà cung cấp. Nhập từ /models hoặc thêm ID tùy chỉnh để định tuyến.", "bedrockModelsDescription": "Các mô hình Amazon Bedrock được giới hạn theo vùng AWS. Nhập từ /models hoặc thêm ID mô hình Bedrock được bật trong vùng đã chọn.", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 1c58b63e59..31d1a87e10 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -3718,11 +3718,7 @@ "errorDescription": "我们现在无法加载组合数据。请检查您的连接并重试。", "errorId": "错误 ID: {id}", "errorRetry": "再试一次", - "comboLabel": "组合", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "组合" }, "costs": { "title": "成本", @@ -6231,8 +6227,7 @@ "kiro": "免费层:50 积分/月(约 25K–100K 令牌)。⚠️ Kiro 服务条款禁止使用第三方代理/测试框架。", "codex": "使用现有的 OAuth 流程连接 OpenAI Codex。", "qwen": "使用现有的 OAuth 流程连接 Qwen Code。", - "github-models": "在 github.com/settings/tokens 创建具有 'models: read' 作用域的 GitHub PAT", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "在 github.com/settings/tokens 创建具有 'models: read' 作用域的 GitHub PAT" }, "passthroughModelsDescription": "{provider} 接受供应商本机模型 ID。从 /models 导入或添加用于路由的自定义 ID。", "bedrockModelsDescription": "Amazon Bedrock 模型的范围按 AWS 区域划分。从 /models 导入或添加在所选区域中启用的基岩模型 ID。", @@ -8637,28 +8632,7 @@ "saved": "已保存。", "saveFailed": "无法保存。", "enableAria": "启用 OmniGlyph 引擎", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "嵌入式服务", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index ddec1a0f6e..6bc5f1954d 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -3718,11 +3718,7 @@ "errorDescription": "目前無法加載組合數據。請檢查您的連接並重試。", "errorId": "錯誤 ID: {id}", "errorRetry": "再試一次", - "comboLabel": "組合", - "usageGuideInvokeTitle": "How to call this combo", - "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", - "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", - "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models." + "comboLabel": "組合" }, "costs": { "title": "成本", @@ -6220,8 +6216,7 @@ "kiro": "免費方案:每月 50 額度(約 2.5 萬至 10 萬 tokens)。⚠️ Kiro 服務條款禁止第三方代理/轉接使用。", "codex": "使用現有的 OAuth 流程連線 OpenAI Codex。", "qwen": "使用現有的 OAuth 流程連線 Qwen Code。", - "github-models": "在 github.com/settings/tokens 建立具有 'models: read' 範圍的 GitHub PAT", - "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed." + "github-models": "在 github.com/settings/tokens 建立具有 'models: read' 範圍的 GitHub PAT" }, "passthroughModelsDescription": "{provider} 接受提供者本機模型 ID。從 /models 匯入或新增用於路由的自定義 ID。", "bedrockModelsDescription": "Amazon Bedrock 模型的範圍按 AWS 區域劃分。從 /models 匯入或新增在所選區域中啟用的基岩模型 ID。", @@ -8637,28 +8632,7 @@ "saved": "已儲存。", "saveFailed": "無法儲存。", "enableAria": "啟用 OmniGlyph 引擎", - "title": "OmniGlyph", - "profileTitle": "Compression profile", - "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", - "profiles": { - "aggressive": { - "label": "Aggressive (default)", - "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." - }, - "balanced": { - "label": "Balanced", - "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." - }, - "codingSafe": { - "label": "Coding-safe", - "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." - }, - "passthrough": { - "label": "Passthrough", - "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." - } - }, - "profileAria": "Select the OmniGlyph compression profile" + "title": "OmniGlyph" }, "embeddedServices": { "title": "內嵌服務", diff --git a/src/lib/modelMetadataRegistry.ts b/src/lib/modelMetadataRegistry.ts index 5e477084ea..c5c0bbbb7c 100644 --- a/src/lib/modelMetadataRegistry.ts +++ b/src/lib/modelMetadataRegistry.ts @@ -29,6 +29,7 @@ import { CANONICAL_EFFORT_VALUES, extendCodexGpt56EffortValues, } from "@/shared/reasoning/effortStandardization"; +import type { ModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot"; const MODEL_METADATA_SCHEMA_VERSION = "model-metadata-v1"; diff --git a/stryker.conf.json b/stryker.conf.json index 59b52acdac..08c35843f5 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -64,7 +64,6 @@ "tests/unit/adobe-firefly.test.ts", "tests/unit/agentrouter-error-rules.test.ts", "tests/unit/agentrouter-lock-scope-10334.test.ts", - "tests/unit/aihorde-optional-api-key.test.ts", "tests/unit/alibaba-free-tier-exhaustion.test.ts", "tests/unit/anthropic-thinking-signature-recovery.test.ts", "tests/unit/antigravity-429-quota-tdd.test.ts", @@ -151,10 +150,8 @@ "tests/unit/codex-failover.test.ts", "tests/unit/codex-quota-selection-hydration.test.ts", "tests/unit/codex-responses-to-chat-9161.test.ts", - "tests/unit/codex-same-account-transport-retry-9708.test.ts", "tests/unit/codex-session-affinity-reset-aware-5903.test.ts", "tests/unit/codex-stream-false.test.ts", - "tests/unit/codex-turn-state.test.ts", "tests/unit/collect-metrics-module-coverage.test.ts", "tests/unit/combo-499-abort.test.ts", "tests/unit/combo-account-allowlist-3266.test.ts", @@ -297,9 +294,6 @@ "tests/unit/quota-pool-log-route.test.ts", "tests/unit/quota-streaming-consumption-usd.test.ts", "tests/unit/qwen-web-content-array-serialization.test.ts", - "tests/unit/probe-gate-autodisable.test.ts", - "tests/unit/probe-production-path.test.ts", - "tests/unit/probe-testall-isolation.test.ts", "tests/unit/rate-limit-enhanced.test.ts", "tests/unit/rate-limit-execution-timeout-message-4165.test.ts", "tests/unit/rate-limit-local-capacity-classification.test.ts", @@ -313,7 +307,6 @@ "tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts", "tests/unit/resilience-connections.test.ts", "tests/unit/responses-handler.test.ts", - "tests/unit/responses-passthrough-openai-compatible.test.ts", "tests/unit/rotation-config-omniroute.test.ts", "tests/unit/route-explainability.test.ts", "tests/unit/route-guard-acp-agents-local-only.test.ts", @@ -342,7 +335,6 @@ "tests/unit/skip-provider-breaker-consumer-2743.test.ts", "tests/unit/sse-auth-antigravity-credits.test.ts", "tests/unit/sse-auth-codex-account-pool.test.ts", - "tests/unit/sse-auth-exclusive-leases.test.ts", "tests/unit/sse-auth-resource-404.test.ts", "tests/unit/sse-auth.test.ts", "tests/unit/db/stats-dbstat-optional.test.ts", diff --git a/tests/unit/check-env-doc-sync.test.ts b/tests/unit/check-env-doc-sync.test.ts index 3975e58361..2db6a62c87 100644 --- a/tests/unit/check-env-doc-sync.test.ts +++ b/tests/unit/check-env-doc-sync.test.ts @@ -179,32 +179,6 @@ test("runEnvDocSync: ignore set skips a code-referenced var", () => { assert.equal(result.ok, true); }); -test("runEnvDocSync: shipped allowlist ignores ad-hoc BOT_TOKEN and BOT_URL", () => { - const envExampleText = `JWT_SECRET=secret\n`; - const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; - const codeVars = new Set(["JWT_SECRET", "BOT_TOKEN", "BOT_URL"]); - - const unignored = runEnvDocSync({ - envExampleText, - envDocText, - codeVars, - ignore: new Set(), - docOnlyAllowlist: new Set(), - envOnlyAllowlist: new Set(), - }); - assert.equal(unignored.ok, false); - assert.deepEqual(unignored.problems.codeMissingEnv, ["BOT_TOKEN", "BOT_URL"]); - - // Omit `ignore` so the checker uses IGNORE_FROM_CODE from check-env-doc-sync.mjs. - const shipped = runEnvDocSync({ - envExampleText, - envDocText, - codeVars, - }); - assert.equal(shipped.ok, true); - assert.deepEqual(shipped.problems.codeMissingEnv, []); -}); - test("repository contract is in sync (live data)", () => { // Uses the real .env.example, docs/ENVIRONMENT.md, and the bundled // allowlists. This is the same check that runs in pre-commit / CI. diff --git a/tests/unit/combo-context-overflow-compression-probe.test.ts b/tests/unit/combo-context-overflow-compression-probe.test.ts index fd602564c9..f482cba352 100644 --- a/tests/unit/combo-context-overflow-compression-probe.test.ts +++ b/tests/unit/combo-context-overflow-compression-probe.test.ts @@ -32,7 +32,9 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const { saveModelsDevCapabilities, clearModelsDevCapabilities } = await import("../../src/lib/modelsDevSync.ts"); -const { handleComboChat } = await import("../../open-sse/services/combo.ts"); +const { getKnownContextOverflow, handleComboChat } = await import( + "../../open-sse/services/combo.ts" +); const { updateCompressionSettings } = await import("../../src/lib/db/compression.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); @@ -72,6 +74,20 @@ function capabilityEntry(limitContext: number | null) { }; } +function target(modelStr: string) { + return { + kind: "model" as const, + stepId: modelStr, + executionKey: modelStr, + modelStr, + provider: modelStr.includes("/") ? modelStr.split("/")[0] : modelStr, + providerId: null, + connectionId: null, + weight: 1, + label: null, + }; +} + // A generic Responses-API body whose estimate lands near `tokens` tokens (4 chars/token). // Uses `input:` (not `messages:`) to mirror the OpenCode/Codex Responses surface. function bigResponsesBody(tokens: number) { @@ -80,6 +96,32 @@ function bigResponsesBody(tokens: number) { const noopLog = { info() {}, warn() {}, error() {}, debug() {} }; +test("#10225 getKnownContextOverflow defers the hard overflow when compression is available", () => { + saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); + const body = bigResponsesBody(275_000); + + // Compression enabled + target can compress -> defer (null). + assert.equal( + getKnownContextOverflow([target("codex/gpt-5.6-terra")], body, { + deferContextOverflowWhenCompressible: true, + }), + null, + "compressible request must defer so chatCore compression can run (#10225)" + ); + + // Compression disabled -> the existing hard overflow is preserved (never lose #7177). + const hard = getKnownContextOverflow([target("codex/gpt-5.6-terra")], body); + assert.ok(hard); + assert.ok(hard.requiredContextTokens > hard.maxKnownContextTokens); + + // Compression enabled but EVERY target is excluded from compression -> keep the hard gate. + const excluded = getKnownContextOverflow([target("codex/gpt-5.6-terra")], body, { + deferContextOverflowWhenCompressible: true, + compressionExclusions: ["gpt-5.6-terra"], + }); + assert.ok(excluded, "fully-excluded targets must retain the hard preflight"); +}); + test("#10225 combo does not early-400 a compressible over-limit request when deferral is on", async () => { saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); let dispatches = 0; @@ -105,7 +147,7 @@ test("#10225 combo does not early-400 a compressible over-limit request when def assert.equal(dispatches, 1, "must dispatch so chatCore compaction runs first"); }); -test("#10225 combo does not hard-400 an over-limit request when compression is disabled", async () => { +test("#10225 combo keeps the fast 400 when compression is disabled", async () => { saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); let dispatches = 0; @@ -126,10 +168,10 @@ test("#10225 combo does not hard-400 an over-limit request when compression is d log: noopLog, }); - // #10162: gateway chars/4 estimates are advisory. Compression off is not a - // pre-dispatch 400; chatCore / upstream remain the context gate. - assert.notEqual(response.status, 400, "advisory estimates must not hard-400 before dispatch (#10162)"); - assert.equal(dispatches, 1, "must dispatch when local overflow estimates are advisory"); + assert.equal(response.status, 400); + assert.equal(dispatches, 0, "#7177 anti-exhaustion guard must survive when compression is off"); + const body = await response.json(); + assert.equal(body.error.code, "context_length_exceeded"); }); // #10501-sweep #10503 — the deferral above is NOT target-aware by default: it only @@ -151,7 +193,47 @@ test("#10225 combo does not hard-400 an over-limit request when compression is d // combo member over `/v1/responses` in openai-responses format still hits chatCore's // compression bypass — exactly the gap `sourceFormat`/`endpointPath` (not the looser // `clientManagedResponsesContext` flag) now closes. -test("#10503 handleComboChat: native-codex-passthrough pool still dispatches oversized requests", async () => { +test("#10503 getKnownContextOverflow REFUSES to defer when the only target is native Codex Responses passthrough", () => { + saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); + const body = bigResponsesBody(275_000); + + const overflow = getKnownContextOverflow([target("codex/gpt-5.6-terra")], body, { + deferContextOverflowWhenCompressible: true, + sourceFormat: "openai-responses", + endpointPath: "/v1/responses", + }); + + assert.ok( + overflow, + "a native-codex-passthrough target must never be treated as compressible — the " + + "hard preflight must stay active (chatCore disables compression for it entirely)" + ); +}); + +test("#10503 getKnownContextOverflow still defers when a genuinely compressible sibling target is present", () => { + saveModelsDevCapabilities({ + codex: { "gpt-5.6-terra": capabilityEntry(272_000) }, + openai: { "gpt-5.6-terra": capabilityEntry(272_000) }, + }); + const body = bigResponsesBody(275_000); + + // A heterogeneous pool where at least ONE target (openai) genuinely runs + // compression must still defer — deferral is a per-request decision, and other + // targets in the pool are unaffected by the codex-specific compression bypass. + const overflow = getKnownContextOverflow( + [target("codex/gpt-5.6-terra"), target("openai/gpt-5.6-terra")], + body, + { + deferContextOverflowWhenCompressible: true, + sourceFormat: "openai-responses", + endpointPath: "/v1/responses", + } + ); + + assert.equal(overflow, null, "a genuinely compressible sibling target must still defer"); +}); + +test("#10503 handleComboChat: native-codex-passthrough pool fails FAST locally, zero upstream dispatches", async () => { saveModelsDevCapabilities({ codex: { "gpt-5.6-terra": capabilityEntry(272_000) } }); let dispatches = 0; @@ -173,14 +255,14 @@ test("#10503 handleComboChat: native-codex-passthrough pool still dispatches ove log: noopLog, }); - // #10162 removed the chars/4 pre-dispatch 400. Native Codex passthrough still - // reaches the target; chatCore / upstream enforce the real context limit. - assert.notEqual( + assert.equal( response.status, 400, - "must not fail fast locally on an advisory overflow estimate (#10162)" + "must fail fast locally instead of dispatching an oversized, uncompressible request" ); - assert.equal(dispatches, 1, "advisory estimates must not skip upstream dispatch"); + assert.equal(dispatches, 0, "no wasted upstream call for a target that can never compress"); + const responseBody = await response.json(); + assert.equal(responseBody.error.code, "context_length_exceeded"); }); // #10503 item 2 — drive the REAL chatCore compression pipeline end-to-end (not just the diff --git a/tests/unit/db-driver-bundling-externals.test.ts b/tests/unit/db-driver-bundling-externals.test.ts index c7dcc0d06c..b1d920dd0e 100644 --- a/tests/unit/db-driver-bundling-externals.test.ts +++ b/tests/unit/db-driver-bundling-externals.test.ts @@ -36,10 +36,7 @@ test("sync driver cascade requires each SQLite module by literal specifier", () // The production loader must be the literal-specifier wrapper, never `_require` // itself — passing `_require` through the `load` parameter is exactly what makes // webpack substitute its missing-module stub. - assert.match( - driverFactory, - /^const openSyncDriver = createSyncDriverFactory\(\w+(?:,[\s\S]*?)?\);$/m - ); + assert.match(driverFactory, /^const openSyncDriver = createSyncDriverFactory\(\w+\);$/m); assert.match(driverFactory, /^export function tryOpenSync\($/m); assert.doesNotMatch(driverFactory, /createSyncDriverFactory\(\s*_require\s*\)/); diff --git a/tests/unit/hard-session-lease-bypass-inventory.test.ts b/tests/unit/hard-session-lease-bypass-inventory.test.ts index 24507e00c8..428b75bdcf 100644 --- a/tests/unit/hard-session-lease-bypass-inventory.test.ts +++ b/tests/unit/hard-session-lease-bypass-inventory.test.ts @@ -15,16 +15,12 @@ const EXPECTED: Record> = { credential: { "open-sse/handlers/chatCore.ts": 1, "open-sse/services/imageCombo.ts": 1, - "open-sse/services/speechCombo.ts": 1, - "open-sse/services/videoCombo.ts": 2, "src/app/api/compression/compare/verify/route.ts": 1, "src/app/api/internal/codex-responses-ws/route.ts": 1, "src/app/api/search/providers/route.ts": 3, - "src/app/api/v1/_shared/videoModelResolution.ts": 1, "src/app/api/v1/audio/speech/route.ts": 1, - "src/app/api/v1/audio/transcriptions/route.ts": 2, + "src/app/api/v1/audio/transcriptions/route.ts": 1, "src/app/api/v1/audio/translations/route.ts": 1, - "src/app/api/v1/classify/route.ts": 1, "src/app/api/v1/images/edits/route.ts": 5, "src/app/api/v1/images/generations/route.ts": 3, "src/app/api/v1/images/upscale/route.ts": 1, @@ -36,9 +32,8 @@ const EXPECTED: Record> = { "src/app/api/v1/providers/[provider]/images/generations/route.ts": 1, "src/app/api/v1/rerank/route.ts": 2, "src/app/api/v1/search/route.ts": 2, - "src/app/api/v1/segment/route.ts": 1, "src/app/api/v1/session-leases/route.ts": 1, - "src/app/api/v1/videos/generations/route.ts": 2, + "src/app/api/v1/videos/generations/route.ts": 3, "src/app/api/v1/web/fetch/route.ts": 1, "src/lib/embeddings/service.ts": 2, "src/lib/memory/embedding/index.ts": 1, @@ -54,7 +49,6 @@ const EXPECTED: Record> = { "open-sse/handlers/chatCore/cliproxyapiCredentials.ts": 1, "open-sse/handlers/imageGeneration.ts": 1, "open-sse/handlers/imageGeneration/providers/chatgptWeb.ts": 1, - "open-sse/handlers/imageGeneration/providers/geminiWeb.ts": 1, "open-sse/handlers/videoGeneration.ts": 1, "open-sse/services/compression/eval/executorModelClient.ts": 1, "src/lib/compression/judgeModelClient.ts": 1, @@ -155,7 +149,6 @@ const CLASSIFICATION: Record> = { "open-sse/handlers/chatCore/cliproxyapiCredentials.ts": "A", "open-sse/handlers/imageGeneration.ts": "B", "open-sse/handlers/imageGeneration/providers/chatgptWeb.ts": "B", - "open-sse/handlers/imageGeneration/providers/geminiWeb.ts": "B", "open-sse/handlers/videoGeneration.ts": "B", "open-sse/services/compression/eval/executorModelClient.ts": "B", "src/lib/compression/judgeModelClient.ts": "B", diff --git a/tests/unit/sse-heartbeat.test.ts b/tests/unit/sse-heartbeat.test.ts index 94f39cfebc..57ecbd9450 100644 --- a/tests/unit/sse-heartbeat.test.ts +++ b/tests/unit/sse-heartbeat.test.ts @@ -1,11 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -// #10524 default is comment-heartbeats off. This file asserts heartbeat payloads, -// so opt in for the suite (restored in process teardown is unnecessary: node:test -// worker is dedicated). -process.env.OMNIROUTE_SSE_COMMENTS = "on"; - const { createSseHeartbeatTransform } = await import("../../open-sse/utils/sseHeartbeat.ts"); function withFakeIntervals(fn) { From 8bc2f0f10c4c8057604b27e4ed476d1966be24bf Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Thu, 20 Aug 2026 11:31:15 +0200 Subject: [PATCH 070/135] chore(ci): ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync scripts/ad-hoc mesh helpers read operator-supplied BOT_TOKEN/BOT_URL. They are not OmniRoute runtime config and should not fail Docs Gates on every PR. Unblocks check:env-doc-sync on release/v3.8.50. --- changelog.d/maintenance/env-doc-sync-adhoc-bot.md | 1 + scripts/check/check-env-doc-sync.mjs | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/maintenance/env-doc-sync-adhoc-bot.md diff --git a/changelog.d/maintenance/env-doc-sync-adhoc-bot.md b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md new file mode 100644 index 0000000000..dbd759be29 --- /dev/null +++ b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md @@ -0,0 +1 @@ +- **chore(ci):** ignore ad-hoc `BOT_TOKEN`/`BOT_URL` in env-doc-sync (scripts/ad-hoc mesh helpers, not runtime config) diff --git a/scripts/check/check-env-doc-sync.mjs b/scripts/check/check-env-doc-sync.mjs index 1fba62bfd5..097b24c60e 100644 --- a/scripts/check/check-env-doc-sync.mjs +++ b/scripts/check/check-env-doc-sync.mjs @@ -126,6 +126,10 @@ const IGNORE_FROM_CODE = new Set([ // ("http://192.168.0.15:20128" / null), never OmniRoute runtime config (#5151). "COMBO_LIVE_BASE_URL", "COMBO_LIVE_API_KEY", + // Ad-hoc mesh/coverage scripts under scripts/ad-hoc/*.mjs (mesh-send, mesh-run, + // verify-coverage). Operator-supplied script secrets, not OmniRoute runtime config. + "BOT_TOKEN", + "BOT_URL", // Homologation E2E suite (npm run homolog) vars — configured via the dedicated // .env.homolog file (template: .env.homolog.example), never in the runtime .env. // Test/ops-only signals against the homologation VPS, same class as COMBO_LIVE_*. From 567b9db04dce68779014400f0ff43a54f3395a76 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:32:11 +0200 Subject: [PATCH 071/135] test(ci): lock env-doc-sync ignore for ad-hoc BOT_TOKEN/BOT_URL Drive runEnvDocSync so BOT_TOKEN/BOT_URL are ignored by IGNORE_FROM_CODE and flagged when ignore is empty. --- tests/unit/check-env-doc-sync.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/check-env-doc-sync.test.ts b/tests/unit/check-env-doc-sync.test.ts index 2db6a62c87..3975e58361 100644 --- a/tests/unit/check-env-doc-sync.test.ts +++ b/tests/unit/check-env-doc-sync.test.ts @@ -179,6 +179,32 @@ test("runEnvDocSync: ignore set skips a code-referenced var", () => { assert.equal(result.ok, true); }); +test("runEnvDocSync: shipped allowlist ignores ad-hoc BOT_TOKEN and BOT_URL", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const codeVars = new Set(["JWT_SECRET", "BOT_TOKEN", "BOT_URL"]); + + const unignored = runEnvDocSync({ + envExampleText, + envDocText, + codeVars, + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(unignored.ok, false); + assert.deepEqual(unignored.problems.codeMissingEnv, ["BOT_TOKEN", "BOT_URL"]); + + // Omit `ignore` so the checker uses IGNORE_FROM_CODE from check-env-doc-sync.mjs. + const shipped = runEnvDocSync({ + envExampleText, + envDocText, + codeVars, + }); + assert.equal(shipped.ok, true); + assert.deepEqual(shipped.problems.codeMissingEnv, []); +}); + test("repository contract is in sync (live data)", () => { // Uses the real .env.example, docs/ENVIRONMENT.md, and the bundled // allowlists. This is the same check that runs in pre-commit / CI. From db7c3abaf6b888ce388844cdd44c1521606a927b Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 12:08:37 -0300 Subject: [PATCH 072/135] fix(images): retry Codex image generation on a sibling ChatGPT account (#10838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (60/60 combined image-generation tests, typecheck:core clean, complexity/cognitive/file-size/changelog gates green). --- ...-codex-image-account-fallback-retryable.md | 1 + open-sse/handlers/imageGeneration.ts | 25 +++++++++ tests/unit/image-generation-handler.test.ts | 54 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 changelog.d/fixes/8307-codex-image-account-fallback-retryable.md diff --git a/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md b/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md new file mode 100644 index 0000000000..bd4a70a1ab --- /dev/null +++ b/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md @@ -0,0 +1 @@ +- **fix(images):** retry Codex image generation on a sibling ChatGPT account when the requested model isn't entitled on the current account, instead of failing the request outright ([#8307](https://github.com/diegosouzapw/OmniRoute/pull/8307)). diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 7fb6357db1..a8452f0245 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -184,6 +184,29 @@ function sanitizeImageProviderError(errorText: string): unknown { return sanitizeErrorMessage(errorText); } +// #8307 — some ChatGPT accounts can run Codex but lack entitlement for the specific +// requested image model. Upstream signals this as a 400 with an exact, stable message +// (not a generic "invalid request"). Classify it so the caller can mark the failure +// `retryable: true`, which routes it through the same sibling-account fallback that +// already handles 401s (executeImageWithCredentialFallback, src/sse/services/imageCredentialRetry.ts). +function isCodexChatGptModelAccessError(status: number, errorText: string, model: string): boolean { + if (status !== 400) return false; + const parsed = parseJsonOrNull(errorText); + let detail: string | null = null; + if (typeof parsed === "string") { + detail = parsed; + } else if (parsed && typeof parsed === "object") { + const obj = parsed as Record; + if (typeof obj.detail === "string") detail = obj.detail; + else if (typeof obj.message === "string") detail = obj.message; + else if (obj.error && typeof obj.error === "object") { + const nested = (obj.error as Record).message; + if (typeof nested === "string") detail = nested; + } + } + return detail === `The '${model}' model is not supported when using Codex with a ChatGPT account.`; +} + const BFL_MODEL_ENDPOINTS = { "flux-2-max": "/v1/flux-2-max", "flux-2-pro": "/v1/flux-2-pro", @@ -2532,6 +2555,7 @@ async function handleCodexImageGeneration({ const safeErrorLog = typeof safeError === "string" ? safeError : JSON.stringify(safeError ?? {}); if (log) log.error("IMAGE", `${provider} error ${response.status}: ${safeErrorLog}`); + const retryable = isCodexChatGptModelAccessError(response.status, errorText, model); return { ok: false as const, error: { @@ -2542,6 +2566,7 @@ async function handleCodexImageGeneration({ error: safeError, requestBody: requestBodyForLog, path: logPath, + ...(retryable ? { retryable: true } : {}), }, }; } diff --git a/tests/unit/image-generation-handler.test.ts b/tests/unit/image-generation-handler.test.ts index d525e60d4d..49709a13c3 100644 --- a/tests/unit/image-generation-handler.test.ts +++ b/tests/unit/image-generation-handler.test.ts @@ -2026,3 +2026,57 @@ test("handleImageGeneration (codex) forwards size and maps GPT-Image quality to globalThis.fetch = originalFetch; } }); + +// #8307 — some ChatGPT accounts can run Codex but lack entitlement for the specific +// requested image model, and the upstream 400 for that exact case is retryable on a +// sibling account: executeImageWithCredentialFallback (route.ts) already retries on +// this signal when the handler marks the failure `retryable: true` — mirroring the +// existing 401 auto-rotate path, no new retry loop needed in the handler itself. +test("handleImageGeneration (codex) marks the ChatGPT-account model-access 400 as retryable", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response( + JSON.stringify({ + error: { + message: + "The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account.", + }, + }), + { status: 400, headers: { "content-type": "application/json" } } + ); + + try { + const result = await handleImageGeneration({ + body: { model: "codex/gpt-5.6-sol", prompt: "kitten" }, + credentials: { accessToken: "codex-token" }, + log: null, + }); + assert.equal(result.success, false); + assert.equal(result.status, 400); + assert.equal(result.retryable, true); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("handleImageGeneration (codex) does not mark an ordinary 400 as retryable", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response(JSON.stringify({ error: { message: "Invalid prompt" } }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + + try { + const result = await handleImageGeneration({ + body: { model: "codex/gpt-5.6-sol", prompt: "kitten" }, + credentials: { accessToken: "codex-token" }, + log: null, + }); + assert.equal(result.success, false); + assert.equal(result.status, 400); + assert.equal(result.retryable, undefined); + } finally { + globalThis.fetch = originalFetch; + } +}); From bbcfb730ca09e3bbcdfa89f3c72a0e918f31319c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 12:08:41 -0300 Subject: [PATCH 073/135] feat(sse): Cursor plan images via Agent CLI (IMAGE_PROVIDERS.cursor) (#10842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (60/60 combined image-generation tests, typecheck:core clean, complexity/cognitive/file-size/changelog gates green, small rebaseline for the combined imageRegistry.ts growth). --- .../features/cursor-agent-image-provider.md | 1 + config/quality/file-size-baseline.json | 5 +- docs/getting-started/PROVIDERS-GUIDE.md | 4 + docs/providers/CURSOR_IMAGE.md | 75 +++ docs/reference/PROVIDER_REFERENCE.md | 4 +- open-sse/config/imageRegistry.ts | 19 + open-sse/handlers/imageGeneration.ts | 18 + .../providers/cursorAgentImage.ts | 488 ++++++++++++++++++ src/app/api/v1/images/generations/route.ts | 7 + tests/unit/cursor-agent-image.test.ts | 297 +++++++++++ 10 files changed, 914 insertions(+), 4 deletions(-) create mode 100644 changelog.d/features/cursor-agent-image-provider.md create mode 100644 docs/providers/CURSOR_IMAGE.md create mode 100644 open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts create mode 100644 tests/unit/cursor-agent-image.test.ts diff --git a/changelog.d/features/cursor-agent-image-provider.md b/changelog.d/features/cursor-agent-image-provider.md new file mode 100644 index 0000000000..84646dc44e --- /dev/null +++ b/changelog.d/features/cursor-agent-image-provider.md @@ -0,0 +1 @@ +- feat(sse): add Cursor plan image generation via Agent CLI (`IMAGE_PROVIDERS.cursor`, format `cursor-agent-image`), reusing the chat Cursor OAuth connection diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 96001706ef..333d269ddc 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -447,7 +447,7 @@ "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "src/lib/modelCapabilities.ts": 1006, "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014, - "open-sse/config/imageRegistry.ts": 1019, + "open-sse/config/imageRegistry.ts": 1033, "src/sse/handlers/chatHelpers.ts": 1017, "src/shared/middleware/chatBodyAdmission.ts": 1005 }, @@ -615,5 +615,6 @@ "_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.", "_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).", "_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).", - "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session)." + "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session).", + "_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines." } \ No newline at end of file diff --git a/docs/getting-started/PROVIDERS-GUIDE.md b/docs/getting-started/PROVIDERS-GUIDE.md index d54b656a8a..65de3c63ea 100644 --- a/docs/getting-started/PROVIDERS-GUIDE.md +++ b/docs/getting-started/PROVIDERS-GUIDE.md @@ -239,3 +239,7 @@ Go to Providers → click on the provider → click **Disconnect**. - **[Free Tiers Guide](./FREE-TIERS-GUIDE.md)** — Get free AI with no credit card - **[Troubleshooting](../guides/TROUBLESHOOTING.md)** — Fix common issues - **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — Full list of 226 providers + +## Cursor images + +Cursor plan images use `IMAGE_PROVIDERS.cursor` (`cursor-agent-image`). See [CURSOR_IMAGE.md](../providers/CURSOR_IMAGE.md). diff --git a/docs/providers/CURSOR_IMAGE.md b/docs/providers/CURSOR_IMAGE.md new file mode 100644 index 0000000000..a620c4de79 --- /dev/null +++ b/docs/providers/CURSOR_IMAGE.md @@ -0,0 +1,75 @@ +--- +title: "Cursor Image Generation" +version: 3.8.49 +lastUpdated: 2026-07-23 +--- + +# Cursor Image Generation + +OmniRoute exposes Cursor plan **image generation** on `POST /v1/images/generations` through the same provider id as chat: `cursor` (alias `cu`). + +| Field | Value | +|-------|--------| +| `IMAGE_PROVIDERS` id | `cursor` | +| Format | `cursor-agent-image` | +| Auth | Same OAuth / API-key connection as chat (`provider_connections.provider = "cursor"`) | +| Models | `cursor/auto`, `cursor/composer-2`, `cursor/composer-2.5` | + +## Why the Agent CLI + +Cursor chat in OmniRoute uses `agent.v1.AgentService/Run` (protobuf). That path **rejects** built-in client tools (shell, write, …). Image generation is a Cursor-native tool executed by the **`agent` CLI** against the seat. The image handler therefore spawns `agent` with a locked prompt and a per-request temp workspace (same shape as community seat bridges), then returns OpenAI-compatible `b64_json`. + +## Access restriction (Hard Rules #15 + #17) + +This is the only `IMAGE_PROVIDERS` format that spawns a child process (the `agent` +binary). Because `POST /v1/images/generations` is shared by ~40 other, non-spawning +image providers that remote callers legitimately use, the whole route is **not** +classified `LOCAL_ONLY` — instead `handleCursorAgentImageGeneration` enforces its own +gate using the trusted `AUTHZ_HEADER_PEER_LOCALITY` verdict the authz pipeline stamps +on every request (from the real TCP peer, never the spoofable `Host` header): only +`loopback` and `lan` callers may reach the spawn; everything else (including a leaked +API key replayed over a public tunnel) gets `403` before any credential lookup or +process spawn happens. See `src/server/authz/policies/management.ts` for the same +policy applied to the rest of the `LOCAL_ONLY` tier. + +## Concurrency gate is module-level (single-instance limitation) + +`CURSOR_IMG_MAX_CONCURRENT` is enforced by an in-memory counter/queue scoped to the +Node module instance (`open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts`). +It correctly limits concurrent `agent` spawns within one OmniRoute process, but does +**not** coordinate across multiple processes/instances sharing the same Cursor seat +(e.g. a multi-replica deployment) — each instance enforces its own independent limit. +For a single-instance deployment (the default) this is exact; horizontally scaled +deployments should keep `CURSOR_IMG_MAX_CONCURRENT` conservative per instance or route +Cursor image traffic to a single instance. + +## Requirements + +1. A connected Cursor account in the dashboard (OAuth or `crsr_…` API key). +2. The Cursor Agent binary available to the OmniRoute process: + - env `CURSOR_AGENT_BIN=/path/to/agent`, or + - `~/.local/bin/agent`, or + - `providerSpecificData.agentBin` on the Cursor connection. + +Optional tuning: + +| Env | Default | Meaning | +|-----|---------|---------| +| `CURSOR_IMG_TIMEOUT_MS` | `210000` | Per-image wall clock | +| `CURSOR_IMG_MAX_CONCURRENT` | `2` | Shared-seat concurrency gate | +| `CURSOR_IMG_MODEL` | (request model / `auto`) | Override CLI `--model` | + +## Example + +```bash +curl -sS https:///v1/images/generations \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"model":"cursor/auto","prompt":"a lantern in fog","size":"1024x1024"}' +``` + +Generation typically takes 1–2 minutes. Prefer an internal network path; edge proxies with ~100s timeouts will fail. + +## LiteLLM + +Register an image model with `mode: image_generation`, `api_base: http://omniroute:20128/v1`, and `model: openai/cursor/auto` (or bare `cursor/auto` depending on your LiteLLM version). diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index 440088ed3d..f6b2568af5 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -62,8 +62,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `clinepass` | `cp` | ClinePass | OAuth | [link](https://cline.bot/cline-pass) | ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow. | | `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. | | `codex` | `cx` | OpenAI Codex | OAuth | — | — | -| `cursor` | `cu` | Cursor IDE | OAuth | — | — | -| `devin-cli` | `dv` | Devin CLI | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai | +| `cursor` | `cu` | Cursor IDE | OAuth, image | — | Image via Agent CLI (`CURSOR_AGENT_BIN`); same seat as chat | +| `devin-cli` | `dv` | Devin CLI (Official) | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai | | `devin-desktop` | — | Devin Desktop | OAuth | [link](https://devin.ai) | Paste an existing Devin API key from an authenticated Devin session. Key export availability and steps vary by Devin version and account. | | `ghe-copilot` | `ghe-copilot` | GitHub Enterprise Copilot | OAuth | — | Enter your GHE instance URL (e.g., https://ghe.company.com) in provider settings, then authenticate via device flow. | | `github` | `gh` | GitHub Copilot | OAuth | — | — | diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 17ce7de90e..8defd6d8a8 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -268,6 +268,25 @@ export const IMAGE_PROVIDERS: Record = { supportedSizes: ["1024x1024", "1024x1536", "1536x1024"], }, + // Cursor plan image generation via the Agent CLI native `generateImage` tool. + // Reuses the same OAuth/API-key connection as chat (`provider: "cursor"`). + // Requires the `agent` binary (CURSOR_AGENT_BIN) — see cursorAgentImage handler. + cursor: { + id: "cursor", + alias: "cu", + // Sentinel: execution is local Agent CLI, not an HTTP image API. + baseUrl: "agent://cursor-agent", + authType: "oauth", + authHeader: "bearer", + format: "cursor-agent-image", + models: [ + { id: "auto", name: "Cursor Auto (Image)" }, + { id: "composer-2", name: "Composer 2 (Image)" }, + { id: "composer-2.5", name: "Composer 2.5 (Image)" }, + ], + supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "1024x1536", "1536x1024"], + }, + "microsoft-designer-web": { id: "microsoft-designer-web", alias: "msdesigner", diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index a8452f0245..5d50f125f9 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -54,6 +54,7 @@ import { handleGeminiWebImageGeneration } from "./imageGeneration/providers/gemi import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts"; import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmind.ts"; import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts"; +import { handleCursorAgentImageGeneration } from "./imageGeneration/providers/cursorAgentImage.ts"; import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts"; import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts"; import { handleAlibabaImageGeneration } from "./imageGeneration/providers/alibabaImage.ts"; @@ -300,6 +301,10 @@ const FAL_PRESET_SIZES = { * @param {object} options.credentials - Provider credentials { apiKey, accessToken } * @param {object} options.log - Logger * @param {string} [options.resolvedProvider] - Pre-resolved provider ID (from route layer custom model resolution) + * @param {string|null} [options.peerLocality] - Trusted "loopback"|"lan"|"remote" verdict + * forwarded from `AUTHZ_HEADER_PEER_LOCALITY` (src/server/authz/headers.ts). Only consumed by + * spawn-capable providers (e.g. cursor-agent-image) to enforce Hard Rules #15/#17 without + * loopback-gating the whole route for every non-spawning image provider. */ export async function handleImageGeneration({ body, @@ -308,6 +313,7 @@ export async function handleImageGeneration({ resolvedProvider = null, signal = null, clientHeaders = null, + peerLocality = null, }) { let provider, model; @@ -518,6 +524,18 @@ export async function handleImageGeneration({ }); } + if (providerConfig.format === "cursor-agent-image") { + return handleCursorAgentImageGeneration({ + model, + provider, + providerConfig, + body, + credentials, + log, + peerLocality, + }); + } + if (providerConfig.format === "designer-web") { return handleDesignerWebImageGeneration({ model, diff --git a/open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts b/open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts new file mode 100644 index 0000000000..a05b7ef854 --- /dev/null +++ b/open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts @@ -0,0 +1,488 @@ +/** + * Cursor Agent image generation — OpenAI `/v1/images/generations` backed by the + * Cursor Agent CLI's native `generateImage` tool (real diffusion, not SVG). + * + * Why CLI (not AgentService/Run): OmniRoute's Cursor chat executor talks to + * `agent.v1.AgentService/Run` over protobuf and **rejects** built-in tools + * (shell/write/…). Image generation is a Cursor-native client tool that the + * `agent` binary executes locally against the seat. Spawning the CLI with a + * locked prompt + per-request workspace mirrors the proven seat bridge shape + * and reuses the same `provider_connections` row as chat (`provider: "cursor"`). + * + * Auth: `credentials.accessToken` / `apiKey` from the Cursor OAuth (or API-key) + * connection. Tokens matching `crsr_…` are exported as `CURSOR_API_KEY`; other + * session JWTs as `CURSOR_AUTH_TOKEN`. The `account::token` composite used by + * the chat executor is normalized the same way (`split("::")[1]`). + * + * Binary: `CURSOR_AGENT_BIN` → `providerSpecificData.agentBin` → PATH / default + * shim under `~/.local/bin/agent`. Missing binary → HTTP 501 with install hint. + */ + +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { sanitizeErrorMessage } from "../../../utils/error.ts"; +import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts"; +import { IMAGE_PROVIDERS } from "../../../config/imageRegistry.ts"; + +export const CURSOR_AGENT_IMAGE_FORMAT = "cursor-agent-image"; + +const DEFAULT_TIMEOUT_MS = 210_000; +const DEFAULT_MAX_CONCURRENT = 2; +const DEFAULT_MODEL = "auto"; +const MAX_N = 4; + +// Upper bound on a caller-supplied `timeout_ms`. The Cursor seat is shared and +// CURSOR_IMG_MAX_CONCURRENT defaults to only 2 slots, so a huge per-request +// timeout must not hog a slot and starve every other caller. +const MAX_TIMEOUT_MS = 300_000; + +// Models the Agent CLI `--model` argv may receive — kept in sync with the +// registry entry (auto | composer-2 | composer-2.5). The request `model` is +// untrusted input forwarded straight into a spawned CLI, so we mirror the +// auggie executor: anything outside this set (unknown model, or a flag-shaped +// value like "--foo" / "-x") is clamped to DEFAULT_MODEL and never reaches argv. +const CURSOR_IMAGE_MODEL_ALLOWLIST: ReadonlySet = new Set( + (IMAGE_PROVIDERS.cursor?.models ?? []).map((m) => m.id) +); + +/** Clamp a model candidate to the allowlist; unknown/flag-shaped → "auto". */ +export function resolveCursorImageModel(candidate: unknown): string { + const requested = typeof candidate === "string" ? candidate.trim() : ""; + return CURSOR_IMAGE_MODEL_ALLOWLIST.has(requested) ? requested : DEFAULT_MODEL; +} + +const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); +const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]); + +/** + * Localities allowed to trigger the `agent` binary spawn below (Hard Rules + * #15 + #17). `/v1/images/generations` is a normal remote-reachable inference + * route shared by ~40 image providers that only proxy HTTP — the ONLY branch + * here that spawns a child process is this one, so the whole route cannot be + * classified in `LOCAL_ONLY_API_PREFIXES` (routeGuard.ts) without blocking + * every other, non-spawning image provider for remote callers. Instead this + * handler enforces its OWN loopback/LAN gate using the trusted locality + * verdict the authz pipeline already stamps on every request + * (`AUTHZ_HEADER_PEER_LOCALITY`, src/server/authz/headers.ts, computed from + * the real TCP peer IP — never the spoofable Host header). Mirrors the + * loopback-or-private-LAN policy `managementPolicy` applies to every other + * LOCAL_ONLY route (src/server/authz/policies/management.ts). + */ +const SPAWN_ALLOWED_LOCALITIES = new Set(["loopback", "lan"]); + +/** Locked instruction — ingress callers can only trigger image gen, never a shell. */ +export function buildCursorAgentImagePrompt(userPrompt: string, outPath: string, size?: unknown): string { + const sizeHint = + typeof size === "string" && size.trim() ? ` Target size/aspect: ${size.trim()}.` : ""; + return [ + "You have a native image-generation tool. Use it to generate ONE image.", + "Do NOT write code, do NOT hand-author SVG, do NOT install packages — use your built-in image generation.", + `Image to generate: ${userPrompt}.${sizeHint}`, + `Save the resulting image to exactly this path: ${outPath}.`, + "When the file exists at that exact path, reply with only the word DONE.", + ].join(" "); +} + +/** Strip OmniRoute `account::token` composites the same way CursorExecutor does. */ +export function normalizeCursorSeatToken(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed) return trimmed; + return trimmed.includes("::") ? trimmed.split("::").slice(1).join("::").trim() || trimmed : trimmed; +} + +/** + * Map a Cursor connection token into the env vars the Agent CLI reads. + * Prefer API keys (`crsr_…`) as `CURSOR_API_KEY`; otherwise session JWT → `CURSOR_AUTH_TOKEN`. + */ +export function buildCursorAgentAuthEnv(token: string): Record { + const clean = normalizeCursorSeatToken(token); + if (clean.startsWith("crsr_")) { + return { CURSOR_API_KEY: clean }; + } + return { CURSOR_AUTH_TOKEN: clean }; +} + +export function resolveCursorAgentBin(override?: string | null): string | null { + // Explicit connection override wins even when the path is missing — the handler + // returns 501 so operators see a clear misconfiguration instead of a silent fallback. + if (typeof override === "string" && override.trim()) { + return override.trim(); + } + const envBin = process.env.CURSOR_AGENT_BIN?.trim(); + if (envBin) return envBin; + + const defaultShim = join(homedir(), ".local", "bin", "agent"); + if (existsSync(defaultShim)) return defaultShim; + + // Last resort: bare `agent` on PATH (spawn fails with ENOENT → 501). + return "agent"; +} + +export function isRasterImageBuffer(buf: Buffer): boolean { + if (buf.length >= 8 && buf.subarray(0, 8).equals(PNG_MAGIC)) return true; + if (buf.length >= 3 && buf.subarray(0, 3).equals(JPEG_MAGIC)) return true; + return false; +} + +export async function findCursorAgentImageOutput( + workspace: string, + preferredPath: string +): Promise { + if (existsSync(preferredPath)) return preferredPath; + try { + const entries = await readdir(workspace); + const match = entries.find((name) => /\.(png|jpe?g|webp)$/i.test(name)); + return match ? join(workspace, match) : null; + } catch { + return null; + } +} + +function normalizePositiveInt(value: unknown, fallback: number, max?: number): number { + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) return fallback; + const i = Math.floor(n); + return typeof max === "number" ? Math.min(i, max) : i; +} + +/** + * Effective per-image wall clock: a caller-supplied `timeout_ms` clamped to + * MAX_TIMEOUT_MS. When the request omits it, fall back to the operator default + * (CURSOR_IMG_TIMEOUT_MS) / DEFAULT_TIMEOUT_MS uncapped — operator config is + * trusted; only the untrusted request value is clamped. + */ +export function resolveCursorImageTimeoutMs(rawTimeout: unknown): number { + return normalizePositiveInt( + rawTimeout, + normalizePositiveInt(process.env.CURSOR_IMG_TIMEOUT_MS, DEFAULT_TIMEOUT_MS), + MAX_TIMEOUT_MS + ); +} + +type CursorAgentImageCredentials = { + apiKey?: string; + accessToken?: string; + providerSpecificData?: Record | null; +}; + +function extractSeatToken(credentials: CursorAgentImageCredentials): string { + const raw = credentials?.accessToken || credentials?.apiKey || ""; + return typeof raw === "string" ? raw.trim() : ""; +} + +function extractAgentBinOverride(credentials: CursorAgentImageCredentials): string | null { + const psd = credentials?.providerSpecificData; + if (!psd || typeof psd !== "object" || Array.isArray(psd)) return null; + const bin = psd.agentBin; + return typeof bin === "string" && bin.trim() ? bin.trim() : null; +} + +function extractAgentModel(credentials: CursorAgentImageCredentials, requestModel: string): string { + const psd = credentials?.providerSpecificData; + if (psd && typeof psd === "object" && !Array.isArray(psd)) { + const fromPsd = psd.imageModel; + if (typeof fromPsd === "string" && fromPsd.trim()) return fromPsd.trim(); + } + if (process.env.CURSOR_IMG_MODEL?.trim()) return process.env.CURSOR_IMG_MODEL.trim(); + // The request's `model=cursor/<…>` field is untrusted and flows into the CLI + // `--model` argv — clamp it to the registry allowlist (unknown/flag-shaped → + // "auto"). The operator overrides above (connection psd / CURSOR_IMG_MODEL) + // are trusted deployment config and pass through unchanged. + return resolveCursorImageModel( + requestModel && requestModel !== "cursor" ? requestModel : DEFAULT_MODEL + ); +} + +// ─── process-wide concurrency gate (one shared Cursor seat) ───────────────── + +type Waiter = () => void; +let activeGenerations = 0; +const waitQueue: Waiter[] = []; + +export function __resetCursorAgentImageConcurrencyForTests(): void { + activeGenerations = 0; + waitQueue.length = 0; +} + +function maxConcurrent(): number { + return normalizePositiveInt(process.env.CURSOR_IMG_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT); +} + +async function acquireSlot(): Promise { + if (activeGenerations < maxConcurrent()) { + activeGenerations += 1; + return; + } + await new Promise((resolve) => { + waitQueue.push(() => { + activeGenerations += 1; + resolve(); + }); + }); +} + +function releaseSlot(): void { + activeGenerations = Math.max(0, activeGenerations - 1); + const next = waitQueue.shift(); + if (next) next(); +} + +export type RunCursorAgentImageOptions = { + agentBin: string; + workspace: string; + prompt: string; + model: string; + authEnv: Record; + timeoutMs: number; + spawnImpl?: typeof spawn; +}; + +/** Spawn `agent -p --force …` and resolve when it exits 0 (or reject on timeout/error). */ +export function runCursorAgentImageProcess(opts: RunCursorAgentImageOptions): Promise<{ + stdout: string; + stderr: string; +}> { + const spawnImpl = opts.spawnImpl ?? spawn; + const args = [ + "-p", + "--force", + "--model", + opts.model, + "--workspace", + opts.workspace, + "--output-format", + "text", + opts.prompt, + ]; + + return new Promise((resolve, reject) => { + const child = spawnImpl(opts.agentBin, args, { + cwd: opts.workspace, + env: { + ...process.env, + ...opts.authEnv, + HOME: process.env.HOME || homedir(), + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + const timer = setTimeout(() => { + child.kill("SIGKILL"); + reject(new Error(`Cursor Agent image generation timed out after ${opts.timeoutMs}ms`)); + }, opts.timeoutMs); + + child.stdout?.on("data", (chunk: Buffer | string) => { + stdout += String(chunk); + }); + child.stderr?.on("data", (chunk: Buffer | string) => { + stderr += String(chunk); + }); + child.on("error", (err) => { + clearTimeout(timer); + reject(err); + }); + child.on("close", (code) => { + clearTimeout(timer); + if (code === 0) { + resolve({ stdout, stderr }); + return; + } + reject( + new Error( + `Cursor Agent exited ${code}: ${(stderr || stdout).trim().slice(0, 400) || "no output"}` + ) + ); + }); + }); +} + +async function generateOneImage(params: { + userPrompt: string; + size: unknown; + agentBin: string; + model: string; + authEnv: Record; + timeoutMs: number; + spawnImpl?: typeof spawn; +}): Promise { + const workspace = await mkdtemp(join(tmpdir(), "omni-cursor-img-")); + const outPath = join(workspace, "out.png"); + const prompt = buildCursorAgentImagePrompt(params.userPrompt, outPath, params.size); + + try { + await runCursorAgentImageProcess({ + agentBin: params.agentBin, + workspace, + prompt, + model: params.model, + authEnv: params.authEnv, + timeoutMs: params.timeoutMs, + spawnImpl: params.spawnImpl, + }); + + const found = await findCursorAgentImageOutput(workspace, outPath); + if (!found) { + throw new Error("Cursor Agent produced no image file in the workspace"); + } + const buf = await readFile(found); + if (!isRasterImageBuffer(buf)) { + throw new Error("Cursor Agent output is not a PNG/JPEG raster"); + } + return buf; + } finally { + await rm(workspace, { recursive: true, force: true }).catch(() => {}); + } +} + +export async function handleCursorAgentImageGeneration({ + model, + provider, + providerConfig: _providerConfig, + body, + credentials, + log, + spawnImpl, + peerLocality, +}: { + model: string; + provider: string; + providerConfig: { baseUrl?: string }; + body: { + prompt?: unknown; + size?: unknown; + n?: unknown; + timeout_ms?: unknown; + }; + credentials: CursorAgentImageCredentials; + log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }; + /** Test seam — defaults to node:child_process.spawn */ + spawnImpl?: typeof spawn; + /** + * Trusted locality verdict ("loopback" | "lan" | "remote") forwarded by the + * route layer from `AUTHZ_HEADER_PEER_LOCALITY` (stamped by the authz + * pipeline from the real TCP peer, never the spoofable Host header). Absent + * or unrecognized → fail closed (treated as "remote"). + */ + peerLocality?: string | null; +}) { + const startTime = Date.now(); + + // Hard Rules #15 + #17: reject before doing ANY other work — credential + // lookup, prompt validation, and the `agent` binary spawn itself must never + // run for a non-loopback/non-LAN caller. A leaked API key tunneled from the + // public internet must not be able to trigger a child-process spawn on the + // OmniRoute host. + if (!peerLocality || !SPAWN_ALLOWED_LOCALITIES.has(peerLocality)) { + return saveImageErrorResult({ + provider, + model, + status: 403, + startTime, + error: + "Cursor Agent image generation spawns a local process and is only available from localhost or the private LAN OmniRoute runs on.", + }); + } + + const prompt = typeof body.prompt === "string" ? body.prompt.trim() : ""; + if (!prompt) { + return saveImageErrorResult({ + provider, + model, + status: 400, + startTime, + error: "Prompt is required for Cursor Agent image generation", + }); + } + + const token = extractSeatToken(credentials); + if (!token) { + return saveImageErrorResult({ + provider, + model, + status: 401, + startTime, + error: "Cursor credentials missing accessToken — reconnect the Cursor provider", + }); + } + + const agentBin = resolveCursorAgentBin(extractAgentBinOverride(credentials)); + if (!agentBin || (agentBin !== "agent" && !existsSync(agentBin))) { + // Bare "agent" may still resolve via PATH; only hard-fail when an explicit path is missing. + if (agentBin !== "agent") { + return saveImageErrorResult({ + provider, + model, + status: 501, + startTime, + error: + "Cursor Agent CLI not found. Install the Cursor `agent` binary and set CURSOR_AGENT_BIN, or set providerSpecificData.agentBin on the Cursor connection.", + }); + } + } + + const timeoutMs = resolveCursorImageTimeoutMs(body.timeout_ms); + const count = normalizePositiveInt(body.n, 1, MAX_N); + const agentModel = extractAgentModel(credentials, model); + const authEnv = buildCursorAgentAuthEnv(token); + + if (log?.info) { + log.info( + "IMAGE", + `${provider}/${model} (cursor-agent-image) | n=${count} model=${agentModel} bin=${agentBin}` + ); + } + + const images: Array<{ b64_json: string; revised_prompt: string }> = []; + + try { + for (let i = 0; i < count; i++) { + await acquireSlot(); + try { + const buf = await generateOneImage({ + userPrompt: prompt, + size: body.size, + agentBin: agentBin || "agent", + model: agentModel, + authEnv, + timeoutMs, + spawnImpl, + }); + images.push({ b64_json: buf.toString("base64"), revised_prompt: prompt }); + } finally { + releaseSlot(); + } + } + + return saveImageSuccessResult({ + provider, + model, + startTime, + images, + }); + } catch (err) { + const errorText = sanitizeErrorMessage(err instanceof Error ? err.message : String(err)); + if (log?.error) { + log.error("IMAGE", `${provider} cursor-agent-image error: ${errorText}`); + } + // ENOENT from spawn → treat as missing CLI + const status = + err && typeof err === "object" && "code" in err && (err as { code?: string }).code === "ENOENT" + ? 501 + : 502; + return saveImageErrorResult({ + provider, + model, + status, + startTime, + error: + status === 501 + ? "Cursor Agent CLI not found on PATH. Set CURSOR_AGENT_BIN to the `agent` binary." + : errorText, + }); + } +} diff --git a/src/app/api/v1/images/generations/route.ts b/src/app/api/v1/images/generations/route.ts index 41916a55e6..aa228a4f75 100644 --- a/src/app/api/v1/images/generations/route.ts +++ b/src/app/api/v1/images/generations/route.ts @@ -31,6 +31,7 @@ import { getSpecialtyModelsResponse } from "@/app/api/v1/_shared/specialtyCatalo import { enforceClientApiRouteAuth } from "@/shared/utils/clientApiRouteAuth"; import { runWithCallLogApiKeyContext } from "@/lib/usage/callLogApiKeyContext"; import { executeImageWithCredentialFallback } from "@/sse/services/imageCredentialRetry"; +import { AUTHZ_HEADER_PEER_LOCALITY } from "@/server/authz/headers"; export const dynamic = "force-dynamic"; @@ -290,6 +291,12 @@ async function postHandler(request, context) { ...(isCustomModel && { resolvedProvider: provider }), signal: request.signal, clientHeaders: publicBaseUrlHeaders(request.headers), + // Trusted "loopback"|"lan"|"remote" verdict stamped by the authz + // pipeline from the real TCP peer (never the spoofable Host + // header). Only the spawn-capable cursor-agent-image provider + // consumes this (Hard Rules #15 + #17) — every other image + // provider ignores it. + peerLocality: request.headers.get(AUTHZ_HEADER_PEER_LOCALITY), }) ); diff --git a/tests/unit/cursor-agent-image.test.ts b/tests/unit/cursor-agent-image.test.ts new file mode 100644 index 0000000000..e4c2cfea2d --- /dev/null +++ b/tests/unit/cursor-agent-image.test.ts @@ -0,0 +1,297 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { EventEmitter } from "node:events"; +import { IMAGE_PROVIDERS, parseImageModel, getImageProvider } from "../../open-sse/config/imageRegistry.ts"; +import { + buildCursorAgentAuthEnv, + buildCursorAgentImagePrompt, + CURSOR_AGENT_IMAGE_FORMAT, + handleCursorAgentImageGeneration, + isRasterImageBuffer, + normalizeCursorSeatToken, + resolveCursorImageModel, + resolveCursorImageTimeoutMs, + __resetCursorAgentImageConcurrencyForTests, +} from "../../open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts"; + +test("cursor is registered in IMAGE_PROVIDERS with cursor-agent-image format", () => { + const entry = IMAGE_PROVIDERS.cursor; + assert.ok(entry, "expected IMAGE_PROVIDERS.cursor"); + assert.equal(entry.id, "cursor"); + assert.equal(entry.alias, "cu"); + assert.equal(entry.format, CURSOR_AGENT_IMAGE_FORMAT); + assert.equal(entry.authType, "oauth"); + assert.equal(entry.authHeader, "bearer"); + assert.ok(entry.models.some((m) => m.id === "auto")); + assert.deepEqual(getImageProvider("cursor"), entry); +}); + +test("parseImageModel resolves cursor/auto and cu/auto to the cursor image provider", () => { + assert.deepEqual(parseImageModel("cursor/auto"), { provider: "cursor", model: "auto" }); + assert.deepEqual(parseImageModel("cu/auto"), { provider: "cursor", model: "auto" }); +}); + +test("normalizeCursorSeatToken strips account:: prefix like CursorExecutor", () => { + assert.equal(normalizeCursorSeatToken("acct::tok_abc"), "tok_abc"); + assert.equal(normalizeCursorSeatToken(" crsr_live "), "crsr_live"); + assert.equal(normalizeCursorSeatToken("a::b::c"), "b::c"); +}); + +test("buildCursorAgentAuthEnv maps crsr_ to CURSOR_API_KEY and JWTs to CURSOR_AUTH_TOKEN", () => { + assert.deepEqual(buildCursorAgentAuthEnv("crsr_abc"), { CURSOR_API_KEY: "crsr_abc" }); + assert.deepEqual(buildCursorAgentAuthEnv("user::crsr_abc"), { CURSOR_API_KEY: "crsr_abc" }); + assert.deepEqual(buildCursorAgentAuthEnv("eyJhbGciOi.jwt"), { + CURSOR_AUTH_TOKEN: "eyJhbGciOi.jwt", + }); +}); + +test("buildCursorAgentImagePrompt locks the agent to native generateImage + exact out path", () => { + const prompt = buildCursorAgentImagePrompt("a red cube", "/tmp/out.png", "1024x1024"); + assert.match(prompt, /native image-generation tool/i); + assert.match(prompt, /Do NOT write code/); + assert.match(prompt, /a red cube/); + assert.match(prompt, /1024x1024/); + assert.match(prompt, /\/tmp\/out\.png/); + assert.match(prompt, /\bDONE\b/); +}); + +test("isRasterImageBuffer accepts PNG and JPEG magics", () => { + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]); + const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0]); + assert.equal(isRasterImageBuffer(png), true); + assert.equal(isRasterImageBuffer(jpeg), true); + assert.equal(isRasterImageBuffer(Buffer.from("not-an-image")), false); +}); + +test("handleCursorAgentImageGeneration rejects empty prompt and missing credentials", async () => { + __resetCursorAgentImageConcurrencyForTests(); + const noPrompt = await handleCursorAgentImageGeneration({ + model: "auto", + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: " " }, + credentials: { accessToken: "crsr_x" }, + peerLocality: "loopback", + }); + assert.equal(noPrompt.success, false); + assert.equal(noPrompt.status, 400); + + const noCreds = await handleCursorAgentImageGeneration({ + model: "auto", + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: "hi" }, + credentials: {}, + peerLocality: "loopback", + }); + assert.equal(noCreds.success, false); + assert.equal(noCreds.status, 401); +}); + +test("handleCursorAgentImageGeneration returns 501 when agentBin path is missing", async () => { + __resetCursorAgentImageConcurrencyForTests(); + const result = await handleCursorAgentImageGeneration({ + model: "auto", + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: "a lantern" }, + credentials: { + accessToken: "crsr_test", + providerSpecificData: { agentBin: "/nonexistent/cursor-agent-bin" }, + }, + peerLocality: "loopback", + }); + assert.equal(result.success, false); + assert.equal(result.status, 501); + assert.match(String(result.error), /CURSOR_AGENT_BIN|agentBin/i); +}); + +// ─── Hard Rules #15 + #17: spawn-capable providers must loopback/LAN-gate ─── + +test("handleCursorAgentImageGeneration rejects a non-loopback/non-LAN caller BEFORE spawning", async () => { + __resetCursorAgentImageConcurrencyForTests(); + let spawnCalled = false; + const spyingSpawn = (() => { + spawnCalled = true; + throw new Error("spawn must never be invoked for a remote caller"); + }) as unknown as typeof import("node:child_process").spawn; + + const result = await handleCursorAgentImageGeneration({ + model: "auto", + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: "a lantern in fog" }, + credentials: { + accessToken: "crsr_test", + providerSpecificData: { agentBin: process.execPath }, + }, + spawnImpl: spyingSpawn, + peerLocality: "remote", + }); + + assert.equal(spawnCalled, false, "spawn must not run for a rejected non-local caller"); + assert.equal(result.success, false); + assert.equal(result.status, 403); + assert.match(String(result.error), /localhost|LAN/i); +}); + +test("handleCursorAgentImageGeneration rejects when peerLocality is missing (fail closed)", async () => { + __resetCursorAgentImageConcurrencyForTests(); + const result = await handleCursorAgentImageGeneration({ + model: "auto", + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: "a lantern in fog" }, + credentials: { accessToken: "crsr_test" }, + }); + assert.equal(result.success, false); + assert.equal(result.status, 403); +}); + +/** + * Minimal fake `spawn` that writes a tiny PNG to the out path embedded in the + * prompt and exits 0 — exercises the success path without a real Cursor Agent. + */ +test("handleCursorAgentImageGeneration returns b64_json via injectable spawn", async () => { + __resetCursorAgentImageConcurrencyForTests(); + const { writeFile, mkdir } = await import("node:fs/promises"); + const path = await import("node:path"); + + const tinyPng = Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + ]); + + const fakeSpawn = ((bin: string, args: string[]) => { + assert.ok(bin, "agent bin required"); + const prompt = args[args.length - 1] || ""; + const marker = "Save the resulting image to exactly this path: "; + const idx = prompt.indexOf(marker); + assert.ok(idx >= 0, "prompt must contain out path"); + const after = prompt.slice(idx + marker.length); + const end = after.indexOf(". When the file exists"); + assert.ok(end > 0, "prompt must end out path before DONE clause"); + const outPath = after.slice(0, end); + const child = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + kill: () => void; + }; + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + child.kill = () => undefined; + queueMicrotask(async () => { + await mkdir(path.dirname(outPath), { recursive: true }); + await writeFile(outPath, tinyPng); + child.emit("close", 0); + }); + return child; + }) as unknown as typeof import("node:child_process").spawn; + + // Use an existing path so the preflight existsSync check passes; spawn is faked. + const result = await handleCursorAgentImageGeneration({ + model: "auto", + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: "a lantern in fog", size: "1024x1024", n: 1 }, + credentials: { + accessToken: "crsr_test", + providerSpecificData: { agentBin: process.execPath }, + }, + spawnImpl: fakeSpawn, + peerLocality: "loopback", + }); + + assert.equal(result.success, true); + assert.ok(result.data?.data?.[0]?.b64_json); + assert.equal(result.data.data[0].b64_json, tinyPng.toString("base64")); +}); + +test("resolveCursorImageModel allows only registry models, clamping everything else to auto", () => { + // The three ids declared in IMAGE_PROVIDERS.cursor.models pass through verbatim. + for (const m of IMAGE_PROVIDERS.cursor.models) { + assert.equal(resolveCursorImageModel(m.id), m.id); + } + // Unknown models and (crucially) flag-shaped / injection-y strings fall back to auto. + assert.equal(resolveCursorImageModel("--dangerously-allow-shell"), "auto"); + assert.equal(resolveCursorImageModel("-p"), "auto"); + assert.equal(resolveCursorImageModel("composer-9"), "auto"); + assert.equal(resolveCursorImageModel(" composer-2 "), "composer-2"); // trimmed, still valid + assert.equal(resolveCursorImageModel(""), "auto"); + assert.equal(resolveCursorImageModel(undefined), "auto"); + assert.equal(resolveCursorImageModel(42), "auto"); +}); + +/** + * End-to-end guard: an odd/flag-shaped `model` from the request must never reach + * the spawned Agent CLI argv — the handler resolves it to "auto" first. + */ +test("handleCursorAgentImageGeneration never forwards a flag-shaped model into CLI argv", async () => { + __resetCursorAgentImageConcurrencyForTests(); + const { writeFile, mkdir } = await import("node:fs/promises"); + const path = await import("node:path"); + + const tinyPng = Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + ]); + + let capturedArgs: string[] = []; + const fakeSpawn = ((_bin: string, args: string[]) => { + capturedArgs = args; + const prompt = args[args.length - 1] || ""; + const marker = "Save the resulting image to exactly this path: "; + const idx = prompt.indexOf(marker); + const after = prompt.slice(idx + marker.length); + const outPath = after.slice(0, after.indexOf(". When the file exists")); + const child = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + kill: () => void; + }; + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + child.kill = () => undefined; + queueMicrotask(async () => { + await mkdir(path.dirname(outPath), { recursive: true }); + await writeFile(outPath, tinyPng); + child.emit("close", 0); + }); + return child; + }) as unknown as typeof import("node:child_process").spawn; + + const result = await handleCursorAgentImageGeneration({ + model: "--dangerously-allow-shell", // untrusted, flag-shaped + provider: "cursor", + providerConfig: { baseUrl: "agent://cursor-agent" }, + body: { prompt: "a lantern in fog", n: 1 }, + credentials: { + accessToken: "crsr_test", + providerSpecificData: { agentBin: process.execPath }, + }, + spawnImpl: fakeSpawn, + peerLocality: "loopback", + }); + + assert.equal(result.success, true); + const modelIdx = capturedArgs.indexOf("--model"); + assert.ok(modelIdx >= 0, "expected --model in the CLI argv"); + assert.equal(capturedArgs[modelIdx + 1], "auto", "flag-shaped model must resolve to auto"); + assert.ok( + !capturedArgs.includes("--dangerously-allow-shell"), + "the raw flag-shaped model must not appear anywhere in argv" + ); +}); + +test("resolveCursorImageTimeoutMs clamps caller timeout_ms to the 300s ceiling", () => { + const prev = process.env.CURSOR_IMG_TIMEOUT_MS; + delete process.env.CURSOR_IMG_TIMEOUT_MS; // isolate from any operator default + try { + assert.equal(resolveCursorImageTimeoutMs(5_000), 5_000); // under the cap: unchanged + assert.equal(resolveCursorImageTimeoutMs(300_000), 300_000); // exactly at the cap + assert.equal(resolveCursorImageTimeoutMs(999_999_999), 300_000); // over the cap: clamped + assert.equal(resolveCursorImageTimeoutMs(-1), 210_000); // invalid → default fallback + assert.equal(resolveCursorImageTimeoutMs(undefined), 210_000); // absent → default fallback + } finally { + if (prev === undefined) delete process.env.CURSOR_IMG_TIMEOUT_MS; + else process.env.CURSOR_IMG_TIMEOUT_MS = prev; + } +}); From b052c91014b6d9f08857382960a084d6e3d3cb72 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 12:10:38 -0300 Subject: [PATCH 074/135] fix(fusion): apply vision-compatibility filter to fusion panel + judge (#10737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (fusion-vision-panel-3378 test green, file-size/changelog gates green) after resolving base-drift against #10842/#10838 (both landed just before). --- config/quality/file-size-baseline.json | 5 +- open-sse/services/combo/dispatchPrelude.ts | 68 +++++++++- tests/unit/fusion-vision-panel-3378.test.ts | 142 ++++++++++++++++++++ 3 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 tests/unit/fusion-vision-panel-3378.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 333d269ddc..77d54f7845 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -447,7 +447,7 @@ "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "src/lib/modelCapabilities.ts": 1006, "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014, - "open-sse/config/imageRegistry.ts": 1033, + "open-sse/config/imageRegistry.ts": 1034, "src/sse/handlers/chatHelpers.ts": 1017, "src/shared/middleware/chatBodyAdmission.ts": 1005 }, @@ -616,5 +616,6 @@ "_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).", "_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).", "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session).", - "_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines." + "_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.", + "_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature." } \ No newline at end of file diff --git a/open-sse/services/combo/dispatchPrelude.ts b/open-sse/services/combo/dispatchPrelude.ts index 8fbaff5c82..c9c62ddbe6 100644 --- a/open-sse/services/combo/dispatchPrelude.ts +++ b/open-sse/services/combo/dispatchPrelude.ts @@ -16,11 +16,18 @@ import { getCachedProviderConnections } from "../../../src/lib/db/readCache"; import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker"; import { fisherYatesShuffle, getNextFromDeck } from "../../../src/shared/utils/shuffleDeck"; import { handleFusionChat, type FusionTuning } from "../fusion.ts"; +import { getResolvedModelCapabilities } from "../modelCapabilities.ts"; +import { errorResponseWithComboDiagnostics } from "../../utils/error.ts"; import { parseModel } from "../model.ts"; import { handlePipelineChat, type PipelineStep } from "../pipeline.ts"; import type { resolveComboSetupConfig } from "../comboConfig.ts"; import { clampComboDepth, MAX_GLOBAL_ATTEMPTS, resolveDelayMs } from "./comboPredicates.ts"; -import { resolveComboRuntimeUnits, resolveComboTargets } from "./comboStructure.ts"; +import { + deriveRequestCompatibilityRequirements, + isVisionIncompatibleTarget, + resolveComboRuntimeUnits, + resolveComboTargets, +} from "./comboStructure.ts"; import { isComboModelVisible } from "./comboVisibility.ts"; import { buildFusionHandleSingleModel, extractFusionPanelSpec } from "./fusionPanel.ts"; import { @@ -395,14 +402,27 @@ export async function tryFusionDispatch(args: { }): Promise { const { cfg, combo, config, strategy, log } = args; const configuredJudge = typeof cfg.judgeModel === "string" ? cfg.judgeModel : undefined; + const judgeFusionRequirements = deriveRequestCompatibilityRequirements(args.body); + // #3378: the judge stays in the original conversation (full history, including + // any image_url blocks) — a judge whose vision support cannot be confirmed is + // exactly as unsafe as an unconfirmed panel member (#8332). Drop it the same + // way an operator-hidden judge is dropped below, so fusion falls back to a + // (vision-confirmed) panel member instead of silently losing the image for + // the synthesis step. + const judgeLacksConfirmedVision = + judgeFusionRequirements.requiresVision && + !!configuredJudge && + getResolvedModelCapabilities(configuredJudge).supportsVision !== true; // The panel is filtered for hidden models by resolveComboTargets, but the // explicit judge is a bare string that never passes through it (#8878). Drop a // hidden judge so fusion falls back to a surviving panel member instead of // dispatching a model the operator hid. const judgeModel = - configuredJudge && !isComboModelVisible(configuredJudge, null, args.hiddenModelsByProvider) - ? undefined - : configuredJudge; + configuredJudge && + !judgeLacksConfirmedVision && + isComboModelVisible(configuredJudge, null, args.hiddenModelsByProvider) + ? configuredJudge + : undefined; const fusionTuning = cfg.fusionTuning && typeof cfg.fusionTuning === "object" ? (cfg.fusionTuning as FusionTuning) @@ -415,12 +435,50 @@ export async function tryFusionDispatch(args: { } if (strategy !== "fusion") return null; - const resolvedFusionTargets = resolveComboTargets( + const allResolvedFusionTargets = resolveComboTargets( combo, args.allCombos, clampComboDepth(config.maxComboDepth), args.hiddenModelsByProvider ); + // #3378 (ported from upstream decolua/9router): every non-fusion combo + // strategy runs candidates through filterTargetsByRequestCompatibility before + // dispatch, which excludes a target whose vision support cannot be *confirmed* + // `=== true` for an image-bearing request (#8332 — unknown is treated the same + // as unsupported, never silently forwarded). Fusion resolved its panel via the + // raw target list and skipped that filter entirely, so a panel member with an + // unrecognized model id (capability lookup misses -> supportsVision !== true) + // still received the unmodified image body while the panel silently lost a + // "confirmed vision" voice. Apply the same exclusion here so the fusion panel + // only fans an image request out to targets with confirmed vision support. + const fusionRequirements = judgeFusionRequirements; + const resolvedFusionTargets = fusionRequirements.requiresVision + ? allResolvedFusionTargets.filter( + (target) => !isVisionIncompatibleTarget(target, fusionRequirements) + ) + : allResolvedFusionTargets; + if (fusionRequirements.requiresVision && resolvedFusionTargets.length === 0) { + log.warn( + "COMBO", + `Combo "${combo.name}" fusion panel has no target with confirmed vision support for this image request — every candidate was excluded (#3378)` + ); + return errorResponseWithComboDiagnostics( + 400, + `No target in combo ${combo.name} has confirmed vision support for this image request`, + { + poolSize: allResolvedFusionTargets.length, + attempted: 0, + excluded: allResolvedFusionTargets.map((target) => ({ + provider: target.provider, + model: target.modelStr, + reason: "vision", + })), + attemptOrder: [], + terminalReason: "capability_mismatch", + }, + { code: "capability_mismatch", type: "invalid_request_error" } + ); + } // extractFusionPanelSpec only understands model strings / combo refs, so the // resolved targets have to be flattened before it runs. Keep them indexed so // the panel can be rehydrated below — dispatching the bare strings strips diff --git a/tests/unit/fusion-vision-panel-3378.test.ts b/tests/unit/fusion-vision-panel-3378.test.ts new file mode 100644 index 0000000000..572891d252 --- /dev/null +++ b/tests/unit/fusion-vision-panel-3378.test.ts @@ -0,0 +1,142 @@ +// Regression guard for upstream decolua/9router#3378: "Fusion combo sometimes +// can't see images even when all models support vision". +// +// Every non-fusion combo strategy runs the request through +// filterTargetsByRequestCompatibility (comboStructure.ts) before dispatch, which +// treats a target whose vision support is not *confirmed* `=== true` (unknown OR +// false) as vision-incompatible and excludes it (#8332). The fusion dispatch +// branch (dispatchPrelude.ts::tryFusionDispatch) resolves its panel via the raw +// resolveComboTargets() and skips that compat filter entirely — so a panel +// member whose model id is unrecognized by the capability registry (and thus +// resolves to supportsVision !== true) still receives the unmodified +// image-bearing body, without any signal that its capability could not be +// confirmed. +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-fusion-vision-3378-")); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "fusion-vision-3378-test-secret"; + +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); +const { saveModelsDevCapabilities, clearModelsDevCapabilities } = await import( + "../../src/lib/modelsDevSync.ts" +); +const { resetAllComboMetrics } = await import("../../open-sse/services/comboMetrics.ts"); +const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); +const { resetAll: resetAllSemaphores } = await import( + "../../open-sse/services/rateLimitSemaphore.ts" +); +const core = await import("../../src/lib/db/core.ts"); + +function createLog() { + return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }; +} + +function okResponse(content: string) { + return new Response(JSON.stringify({ choices: [{ message: { role: "assistant", content } }] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function capabilityEntry(overrides: Record = {}) { + return { + tool_call: true, + reasoning: false, + attachment: false, + structured_output: true, + temperature: true, + modalities_input: JSON.stringify(["text"]), + modalities_output: JSON.stringify(["text"]), + knowledge_cutoff: null, + release_date: null, + last_updated: null, + status: null, + family: null, + open_weights: false, + limit_context: 128000, + limit_input: 128000, + limit_output: 4096, + interleaved_field: null, + ...overrides, + }; +} + +const imageRequestBody = { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { type: "image_url", image_url: { url: "https://example.com/cat.png" } }, + ], + }, + ], +}; + +test.beforeEach(() => { + resetAllComboMetrics(); + resetAllCircuitBreakers(); + resetAllSemaphores(); + clearModelsDevCapabilities(); +}); + +test.after(() => { + resetAllComboMetrics(); + resetAllCircuitBreakers(); + resetAllSemaphores(); + clearModelsDevCapabilities(); + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + if (ORIGINAL_DATA_DIR === undefined) { + delete process.env.DATA_DIR; + } else { + process.env.DATA_DIR = ORIGINAL_DATA_DIR; + } +}); + +test( + "fusion panel must not dispatch an image_url request to a member whose vision " + + "support cannot be confirmed (#3378)", + async () => { + // fusion-vision-a is confirmed vision-capable. fusion-unknown has no + // capability entry at all (unrecognized id) -> getResolvedModelCapabilities + // resolves supportsVision to something other than `true`, exactly like the + // "unknown id silently treated as no vision" failure mode from the upstream + // report. + saveModelsDevCapabilities({ + openai: { + "fusion-vision-a": capabilityEntry({ attachment: true }), + }, + }); + + const dispatched: string[] = []; + const result = await handleComboChat({ + body: imageRequestBody, + combo: { + name: "fusion-vision-panel-3378", + strategy: "fusion", + models: ["openai/fusion-vision-a", "openai/fusion-unknown"], + config: { judgeModel: "openai/fusion-vision-a" }, + }, + handleSingleModel: async (_body, modelStr) => { + dispatched.push(modelStr); + return okResponse(`answer from ${modelStr}`); + }, + log: createLog(), + settings: {}, + allCombos: [], + }); + + assert.ok(result.status < 500, "combo call should not hard-fail"); + assert.ok( + !dispatched.includes("openai/fusion-unknown"), + "a panel member with unconfirmed vision support must never receive the raw image_url body" + ); + } +); From c6a0d09bcd9d0f4714741c4b0eaa2c82c16537b0 Mon Sep 17 00:00:00 2001 From: Aaron Scherer <896295+cryptiklemur@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:59:57 -0500 Subject: [PATCH 075/135] fix(combo): await quota token limit lookup (#10686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated together with related cryptiklemur PRs (typecheck:core clean, complexity/cognitive/file-size/changelog gates green, focused tests passing). Real bug, clean fix, great regression test. Thanks! --- .../10686-combo-quota-token-limit-await.md | 1 + open-sse/services/combo.ts | 10 ++- tests/unit/combo-quota-token-limit.test.ts | 76 +++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/10686-combo-quota-token-limit-await.md create mode 100644 tests/unit/combo-quota-token-limit.test.ts diff --git a/changelog.d/fixes/10686-combo-quota-token-limit-await.md b/changelog.d/fixes/10686-combo-quota-token-limit-await.md new file mode 100644 index 0000000000..a9e7b910e3 --- /dev/null +++ b/changelog.d/fixes/10686-combo-quota-token-limit-await.md @@ -0,0 +1 @@ +- **Combo routing:** await each connection's token limit before reserving quota. The old lookup treated the `Promise` as a connection and dropped `rateLimitOverrides.tpm` ([#10686](https://github.com/diegosouzapw/OmniRoute/pull/10686)). diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 71d1f47384..b6ae5d534a 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -87,7 +87,7 @@ import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts"; import { makeConnectionConcurrencyResolver, lookupPositiveCap } from "./combo/concurrencyCaps.ts"; import { acquireQuotaShareConcurrencySlot } from "./combo/quotaShareConcurrency.ts"; import { canAffordRequest } from "../../src/lib/quota/quotaScheduler.ts"; -import { getCachedProviderConnectionById } from "../../src/lib/localDb.ts"; +import { getCachedProviderConnectionById } from "../../src/lib/db/readCache.ts"; import { orderTargetsByEvalScores } from "./evalRouting.ts"; /** @@ -96,11 +96,13 @@ import { orderTargetsByEvalScores } from "./evalRouting.ts"; * keeps the previously recorded limit (or 0 for a fresh row, meaning "no * budget enforced"). */ -function resolveTargetTokenLimit(target: { connectionId?: string | null }): number | undefined { +async function resolveTargetTokenLimit(target: { + connectionId?: string | null; +}): Promise { const connectionId = target?.connectionId; if (!connectionId) return undefined; try { - const connection = getCachedProviderConnectionById(connectionId); + const connection = await getCachedProviderConnectionById(connectionId); const overrides = (connection as { rateLimitOverrides?: Record | null } | null) ?.rateLimitOverrides; const tpm = overrides?.tpm; @@ -3082,7 +3084,7 @@ async function handleRoundRobinCombo({ try { const { reserveQuota } = await import("../../src/lib/quota/quotaScheduler.ts"); reserveQuota(target.connectionId, modelStr, attemptBody as Record, { - tokenLimit: resolveTargetTokenLimit(target), + tokenLimit: await resolveTargetTokenLimit(target), }); } catch { // best-effort only diff --git a/tests/unit/combo-quota-token-limit.test.ts b/tests/unit/combo-quota-token-limit.test.ts new file mode 100644 index 0000000000..53ecdb91c1 --- /dev/null +++ b/tests/unit/combo-quota-token-limit.test.ts @@ -0,0 +1,76 @@ +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-combo-quota-token-limit-")); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +const ORIGINAL_QUOTA_ROUTING = process.env.OMNIROUTE_QUOTA_AWARE_ROUTING; +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_QUOTA_AWARE_ROUTING = "1"; + +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); +const { getProviderQuota } = await import("../../src/lib/quota/providerQuotaState.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const dbCore = await import("../../src/lib/db/core.ts"); + +const log = { info() {}, warn() {}, debug() {}, error() {} }; + +test.after(() => { + dbCore.resetDbInstance(); + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + if (ORIGINAL_QUOTA_ROUTING === undefined) delete process.env.OMNIROUTE_QUOTA_AWARE_ROUTING; + else process.env.OMNIROUTE_QUOTA_AWARE_ROUTING = ORIGINAL_QUOTA_ROUTING; + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("round-robin quota reservation keeps the connection token limit", async () => { + const connection = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "quota token limit test", + apiKey: "sk-quota-token-limit-test", + rateLimitOverrides: { tpm: 5000 }, + }); + assert.ok(connection?.id); + + const model = "openai/gpt-4o"; + const combo = { + name: "quota-token-limit-test", + strategy: "round-robin", + config: { maxRetries: 0, disableSessionStickiness: true }, + models: [ + { + kind: "model", + provider: "openai", + providerId: "openai", + model: "gpt-4o", + connectionId: connection.id, + id: "quota-token-limit-test-target", + }, + ], + }; + + const result = await handleComboChat({ + body: { + model, + messages: [{ role: "user", content: "reserve this request" }], + max_tokens: 8, + stream: false, + }, + combo, + allCombos: [combo], + isModelAvailable: async () => true, + settings: {}, + log, + handleSingleModel: async () => Response.json({ choices: [{ message: { content: "ok" } }] }), + }); + + assert.equal(result.ok, true); + const snapshot = getProviderQuota(connection.id, model); + assert.equal(snapshot?.known, true); + assert.equal(snapshot?.tokenLimit, 5000); + assert.ok((snapshot?.tokensUsed ?? 0) > 0); +}); From 31031f93ef3e46d5b558df32de547fd5b5e8f299 Mon Sep 17 00:00:00 2001 From: Aaron Scherer <896295+cryptiklemur@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:00:01 -0500 Subject: [PATCH 076/135] fix(analytics): query auto routing from call logs (#10685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated together with related cryptiklemur PRs (typecheck:core clean, gates green). Good catch on the phantom usage_logs table. Thanks! --- src/app/api/analytics/auto-routing/route.ts | 1 - src/lib/db/usageLogs.ts | 38 +++----- src/lib/localDb.ts | 2 +- tests/unit/auto-routing-analytics-db.test.ts | 50 ++++++++++ tests/unit/db-logs-cache-3500.test.ts | 98 +++----------------- 5 files changed, 78 insertions(+), 111 deletions(-) create mode 100644 tests/unit/auto-routing-analytics-db.test.ts diff --git a/src/app/api/analytics/auto-routing/route.ts b/src/app/api/analytics/auto-routing/route.ts index f4a5874b20..27fd7e8350 100644 --- a/src/app/api/analytics/auto-routing/route.ts +++ b/src/app/api/analytics/auto-routing/route.ts @@ -16,7 +16,6 @@ export async function GET(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; try { - // Query usage_logs for auto/ prefix requests const totalRequests = getAutoRoutingTotalCount(); // Variant breakdown diff --git a/src/lib/db/usageLogs.ts b/src/lib/db/usageLogs.ts index 7549f93880..53c572785b 100644 --- a/src/lib/db/usageLogs.ts +++ b/src/lib/db/usageLogs.ts @@ -1,11 +1,6 @@ /** - * db/usageLogs.ts — Read-only aggregation queries over `usage_logs` - * extracted from the /api/analytics/auto-routing route handler. - * - * Hard Rule #5: routes must not embed raw SQL — these queries live here so the - * /api/analytics/auto-routing route can delegate. - * - * Sliced out of #3500 (usage_logs cluster, slice 4). + * Read-only auto-routing aggregations over `call_logs`. + * `requested_model` keeps the client auto/* id after routing resolves a target. */ import { getDbInstance } from "./core"; @@ -19,8 +14,7 @@ export interface AutoRoutingTotalResult { } /** - * Returns the total number of requests routed through auto/ prefix models. - * Matches model = 'auto' OR model LIKE 'auto/%'. + * Returns the number of call-log rows requested through auto/ prefix models. */ export function getAutoRoutingTotalCount(): AutoRoutingTotalResult { const db = getDbInstance(); @@ -28,8 +22,8 @@ export function getAutoRoutingTotalCount(): AutoRoutingTotalResult { .prepare( ` SELECT COUNT(*) as count - FROM usage_logs - WHERE model = 'auto' OR model LIKE 'auto/%' + FROM call_logs + WHERE requested_model = 'auto' OR requested_model LIKE 'auto/%' ` ) .get() as AutoRoutingTotalResult | undefined; @@ -42,11 +36,7 @@ export interface AutoRoutingVariantRow { } /** - * Returns per-variant request counts for auto/ prefix models. - * Variant is derived from the model name: - * 'auto' → 'default' - * 'auto/X' → 'X' - * other → 'other' (should not occur given the WHERE clause) + * Returns per-variant request counts from the client-requested model id. */ export function getAutoRoutingVariantBreakdown(): AutoRoutingVariantRow[] { const db = getDbInstance(); @@ -55,13 +45,13 @@ export function getAutoRoutingVariantBreakdown(): AutoRoutingVariantRow[] { ` SELECT CASE - WHEN model = 'auto' THEN 'default' - WHEN model LIKE 'auto/%' THEN SUBSTR(model, 6) + WHEN requested_model = 'auto' THEN 'default' + WHEN requested_model LIKE 'auto/%' THEN SUBSTR(requested_model, 6) ELSE 'other' END as variant, COUNT(*) as count - FROM usage_logs - WHERE model = 'auto' OR model LIKE 'auto/%' + FROM call_logs + WHERE requested_model = 'auto' OR requested_model LIKE 'auto/%' GROUP BY variant ORDER BY count DESC ` @@ -75,7 +65,7 @@ export interface AutoRoutingTopProviderRow { } /** - * Returns the top 10 providers used for auto/ prefix model requests. + * Returns the top 10 providers used for auto/ prefix requests. */ export function getAutoRoutingTopProviders(): AutoRoutingTopProviderRow[] { const db = getDbInstance(); @@ -83,8 +73,10 @@ export function getAutoRoutingTopProviders(): AutoRoutingTopProviderRow[] { .prepare( ` SELECT provider, COUNT(*) as count - FROM usage_logs - WHERE model = 'auto' OR model LIKE 'auto/%' + FROM call_logs + WHERE (requested_model = 'auto' OR requested_model LIKE 'auto/%') + AND provider IS NOT NULL + AND TRIM(provider) != '' GROUP BY provider ORDER BY count DESC LIMIT 10 diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index c4607dc594..761e2db4eb 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -771,7 +771,7 @@ export type { } from "./db/usageAnalytics"; // --------------------------------------------------------------------------- -// usage_logs — auto-routing analytics (#3500 slice 4) +// call_logs auto-routing analytics (#3500 slice 4) // --------------------------------------------------------------------------- export { getAutoRoutingTotalCount, diff --git a/tests/unit/auto-routing-analytics-db.test.ts b/tests/unit/auto-routing-analytics-db.test.ts new file mode 100644 index 0000000000..8216f535f3 --- /dev/null +++ b/tests/unit/auto-routing-analytics-db.test.ts @@ -0,0 +1,50 @@ +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 originalDataDir = process.env.DATA_DIR; +const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-auto-routing-analytics-")); +process.env.DATA_DIR = testDataDir; + +const core = await import("../../src/lib/db/core.ts"); +const usageLogs = await import("../../src/lib/db/usageLogs.ts"); + +test.before(() => { + core.resetDbInstance(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(testDataDir, { recursive: true, force: true }); + if (originalDataDir === undefined) { + delete process.env.DATA_DIR; + } else { + process.env.DATA_DIR = originalDataDir; + } +}); + +test("auto-routing analytics use requested models from the runtime schema", () => { + const db = core.getDbInstance(); + const insert = db.prepare( + `INSERT INTO call_logs (id, model, requested_model, provider, timestamp) + VALUES (?, ?, ?, ?, ?)` + ); + const timestamp = new Date().toISOString(); + + insert.run("auto-default", "claude-opus-5", "auto", "anthropic", timestamp); + insert.run("auto-fast-1", "gpt-5.6-luna", "auto/fast", "openai", timestamp); + insert.run("auto-fast-2", "gpt-5.6-terra", "auto/fast", "openai", timestamp); + insert.run("direct", "gpt-4", "gpt-4", "openai", timestamp); + + assert.deepEqual(usageLogs.getAutoRoutingTotalCount(), { count: 3 }); + assert.deepEqual(usageLogs.getAutoRoutingVariantBreakdown(), [ + { variant: "fast", count: 2 }, + { variant: "default", count: 1 }, + ]); + assert.deepEqual(usageLogs.getAutoRoutingTopProviders(), [ + { provider: "openai", count: 2 }, + { provider: "anthropic", count: 1 }, + ]); +}); diff --git a/tests/unit/db-logs-cache-3500.test.ts b/tests/unit/db-logs-cache-3500.test.ts index 6377bbe3d0..914a3f9a6b 100644 --- a/tests/unit/db-logs-cache-3500.test.ts +++ b/tests/unit/db-logs-cache-3500.test.ts @@ -1,5 +1,5 @@ /** - * #3500 — usage_logs / semantic_cache / proxy_logs SQL extracted into db modules + * #3500: semantic_cache / proxy_logs SQL extracted into db modules * (Hard Rule #5, slice 4). * * Seeds an in-memory temp SQLite DB for each table and asserts each new db @@ -17,35 +17,9 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-db-logs-cache- process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); -const usageLogs = await import("../../src/lib/db/usageLogs.ts"); const semanticCache = await import("../../src/lib/db/semanticCache.ts"); const proxyLogs = await import("../../src/lib/db/proxyLogs.ts"); -// --------------------------------------------------------------------------- -// Helpers — usage_logs seeding -// usage_logs is NOT in the core.ts schema; create it as a lightweight table -// mirroring the columns used by the auto-routing queries (model, provider). -// --------------------------------------------------------------------------- - -function ensureUsageLogsTable() { - const db = core.getDbInstance(); - db.prepare( - `CREATE TABLE IF NOT EXISTS usage_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - model TEXT NOT NULL, - provider TEXT NOT NULL, - timestamp TEXT NOT NULL - )` - ).run(); -} - -function insertUsageLog(row: { model: string; provider: string }) { - const db = core.getDbInstance(); - db.prepare( - `INSERT INTO usage_logs (model, provider, timestamp) VALUES (?, ?, ?)` - ).run(row.model, row.provider, new Date().toISOString()); -} - // --------------------------------------------------------------------------- // Helpers — semantic_cache seeding // --------------------------------------------------------------------------- @@ -70,7 +44,7 @@ function insertSemanticCache(row: { "hash_" + row.id, "{}", row.tokens_saved ?? 0, - row.hit_count ?? 0, + row.hit_count ?? 0 ); } @@ -91,7 +65,6 @@ function insertProxyLog(row: { id: string; timestamp: string; provider?: string test.before(() => { core.resetDbInstance(); - ensureUsageLogsTable(); }); test.after(() => { @@ -99,61 +72,6 @@ test.after(() => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -// =========================================================================== -// usageLogs — getAutoRoutingTotalCount -// =========================================================================== - -test("#3500 getAutoRoutingTotalCount — returns 0 when no rows", () => { - const result = usageLogs.getAutoRoutingTotalCount(); - assert.equal(result.count, 0); -}); - -test("#3500 getAutoRoutingTotalCount — counts auto and auto/* models", () => { - insertUsageLog({ model: "auto", provider: "openai" }); - insertUsageLog({ model: "auto/fast", provider: "anthropic" }); - insertUsageLog({ model: "gpt-4", provider: "openai" }); // must NOT be counted - - const result = usageLogs.getAutoRoutingTotalCount(); - assert.ok(result.count >= 2, `expected >= 2, got ${result.count}`); -}); - -// =========================================================================== -// usageLogs — getAutoRoutingVariantBreakdown -// =========================================================================== - -test("#3500 getAutoRoutingVariantBreakdown — maps auto → default, auto/X → X", () => { - // Insert another auto and auto/fast to have stable counts - insertUsageLog({ model: "auto", provider: "openai" }); - insertUsageLog({ model: "auto/fast", provider: "anthropic" }); - - const rows = usageLogs.getAutoRoutingVariantBreakdown(); - const byVariant: Record = {}; - for (const r of rows) byVariant[r.variant] = r.count; - - assert.ok("default" in byVariant, "should have a 'default' variant for bare 'auto'"); - assert.ok("fast" in byVariant, "should have a 'fast' variant for 'auto/fast'"); - assert.ok(byVariant["default"] >= 1, "default count >= 1"); - assert.ok(byVariant["fast"] >= 1, "fast count >= 1"); -}); - -// =========================================================================== -// usageLogs — getAutoRoutingTopProviders -// =========================================================================== - -test("#3500 getAutoRoutingTopProviders — returns top providers for auto/* models", () => { - const rows = usageLogs.getAutoRoutingTopProviders(); - assert.ok(Array.isArray(rows), "result is array"); - assert.ok(rows.length > 0, "at least one provider row"); - for (const r of rows) { - assert.equal(typeof r.provider, "string"); - assert.equal(typeof r.count, "number"); - } - // Should be ordered descending by count (first row has highest count) - if (rows.length > 1) { - assert.ok(rows[0].count >= rows[1].count, "ordered descending by count"); - } -}); - // =========================================================================== // semanticCache — listSemanticCacheEntries // =========================================================================== @@ -316,8 +234,16 @@ test("#3500 exportProxyLogsSince — returns rows with timestamp >= since", () = const base = new Date("2025-01-15T10:00:00.000Z"); const old = new Date("2025-01-14T10:00:00.000Z"); - insertProxyLog({ id: "pl-new-1", timestamp: new Date("2025-01-15T11:00:00.000Z").toISOString(), provider: "openai" }); - insertProxyLog({ id: "pl-new-2", timestamp: new Date("2025-01-15T12:00:00.000Z").toISOString(), provider: "anthropic" }); + insertProxyLog({ + id: "pl-new-1", + timestamp: new Date("2025-01-15T11:00:00.000Z").toISOString(), + provider: "openai", + }); + insertProxyLog({ + id: "pl-new-2", + timestamp: new Date("2025-01-15T12:00:00.000Z").toISOString(), + provider: "anthropic", + }); insertProxyLog({ id: "pl-old-1", timestamp: old.toISOString(), provider: "openai" }); // outside window const rows = proxyLogs.exportProxyLogsSince(base.toISOString()); From 06315c445c40b821830bfeb23ee6043740c3c815 Mon Sep 17 00:00:00 2001 From: Aaron Scherer <896295+cryptiklemur@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:00:05 -0500 Subject: [PATCH 077/135] test(aihorde): guard client browser bundles (#10682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (typecheck:core clean, gates green). Thanks for the extra regression coverage! --- .../media-page-client-browser-bundle.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/unit/media-page-client-browser-bundle.test.ts diff --git a/tests/unit/media-page-client-browser-bundle.test.ts b/tests/unit/media-page-client-browser-bundle.test.ts new file mode 100644 index 0000000000..74c4656b29 --- /dev/null +++ b/tests/unit/media-page-client-browser-bundle.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { build } from "esbuild"; + +const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url)); + +async function assertBrowserBundleSafe(relativePath: string) { + await assert.doesNotReject( + build({ + absWorkingDir: REPO_ROOT, + entryPoints: [fileURLToPath(new URL(relativePath, import.meta.url))], + bundle: true, + format: "esm", + logLevel: "silent", + platform: "browser", + tsconfig: "tsconfig.json", + write: false, + }) + ); +} + +test("media page client entry stays browser-bundle safe", async () => { + await assertBrowserBundleSafe( + "../../src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx" + ); +}); + +test("provider detail client entry stays browser-bundle safe", async () => { + await assertBrowserBundleSafe( + "../../src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx" + ); +}); From 2a10d16114e7abb66021d4f0d0a663b10d7d574f Mon Sep 17 00:00:00 2001 From: Aaron Scherer <896295+cryptiklemur@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:00:34 -0500 Subject: [PATCH 078/135] test(chatcore): wait for queued call log writes (#10683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (70/70 combined chatcore-translation-paths tests, typecheck:core clean, gates green). Thanks! --- tests/unit/chatcore-translation-paths.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index 482f273332..db081c9345 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -36,7 +36,8 @@ const { setBackgroundDegradationConfig, resetStats: resetBackgroundStats, } = await import("../../open-sse/services/backgroundTaskDetector.ts"); -const { getCallLogs, getCallLogById } = await import("../../src/lib/usage/callLogs.ts"); +const { getCallLogs, getCallLogById, waitForCallLogSaves } = + await import("../../src/lib/usage/callLogs.ts"); const { handleChatCore, shouldUseNativeCodexPassthrough, @@ -286,6 +287,7 @@ async function flushAsyncSideEffects() { } async function getLatestCallLog() { + await waitForCallLogSaves(5000); const rows = await getCallLogs({ limit: 5 }); if (!Array.isArray(rows) || rows.length === 0) return null; return getCallLogById(rows[0].id); From 22e46a0875f9feb400bc9e1417ea8f82e6ec757d Mon Sep 17 00:00:00 2001 From: Aaron Scherer <896295+cryptiklemur@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:05:46 -0500 Subject: [PATCH 079/135] fix(sse): advance Claude cache breakpoints on growing tails (#10684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (110/110 focused tests, typecheck:core clean, gates green) after resolving base-drift against #10683 (both landed today, same test file — combined). Nice, well-tested cache-breakpoint fix. Thanks! --- open-sse/services/claudeCodeConstraints.ts | 58 +++++++++---------- tests/unit/chatcore-translation-paths.test.ts | 30 +++++++--- tests/unit/claude-code-parity.test.ts | 47 ++++++++++++++- 3 files changed, 96 insertions(+), 39 deletions(-) diff --git a/open-sse/services/claudeCodeConstraints.ts b/open-sse/services/claudeCodeConstraints.ts index 42ed52f996..094727e11b 100644 --- a/open-sse/services/claudeCodeConstraints.ts +++ b/open-sse/services/claudeCodeConstraints.ts @@ -130,27 +130,34 @@ export function ensureCacheControlOnLastUserMessage(body: Record> | undefined; - const systemCacheControlCount = Array.isArray(system) + let cacheControlCount = Array.isArray(system) ? system.filter((block) => block.cache_control).length : 0; + let hasFiveMinuteCacheControl = Array.isArray(system) + ? system.some( + (block) => (block.cache_control as Record | undefined)?.ttl === "5m" + ) + : false; for (const message of messages) { const content = message.content as Array> | undefined; - if (Array.isArray(content) && content.some((block) => block.cache_control)) { - return; - } + if (!Array.isArray(content)) continue; + cacheControlCount += content.filter((block) => block.cache_control).length; + hasFiveMinuteCacheControl ||= content.some( + (block) => (block.cache_control as Record | undefined)?.ttl === "5m" + ); } - if (systemCacheControlCount >= MAX_CACHE_CONTROL_BLOCKS) return; - // Find the last user message for (let i = messages.length - 1; i >= 0; i--) { if (String(messages[i].role) === "user") { const content = messages[i].content; if (Array.isArray(content) && content.length > 0) { const lastBlock = content[content.length - 1] as Record; - if (!lastBlock.cache_control) { - lastBlock.cache_control = { type: "ephemeral" }; + if (!lastBlock.cache_control && cacheControlCount < MAX_CACHE_CONTROL_BLOCKS) { + lastBlock.cache_control = hasFiveMinuteCacheControl + ? { type: "ephemeral", ttl: "5m" } + : { type: "ephemeral" }; } } break; @@ -158,38 +165,31 @@ export function ensureCacheControlOnLastUserMessage(body: Record): void { + let hasFiveMinuteCacheControl = false; + const defaultMissingTtl = (block: Record | null | undefined) => { const cc = block?.cache_control as Record | undefined; - if (cc && cc.type === "ephemeral" && cc.ttl === undefined) { - cc.ttl = "1h"; + if (!cc || cc.type !== "ephemeral") return; + + if (cc.ttl === "5m") { + hasFiveMinuteCacheControl = true; + } else if (cc.ttl === undefined) { + cc.ttl = hasFiveMinuteCacheControl ? "5m" : "1h"; } }; - const system = body.system as Array> | undefined; - if (Array.isArray(system)) { - for (const block of system) defaultMissingTtl(block); - } - const tools = body.tools as Array> | undefined; if (Array.isArray(tools)) { for (const tool of tools) defaultMissingTtl(tool); } + const system = body.system as Array> | undefined; + if (Array.isArray(system)) { + for (const block of system) defaultMissingTtl(block); + } + const messages = body.messages as Array> | undefined; if (Array.isArray(messages)) { for (const message of messages) { diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index db081c9345..8ae3d4d36d 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -1085,13 +1085,16 @@ test("chatCore preserves Opus 5 mid-conversation system cache breakpoints", asyn ); assert.deepEqual(call.body.messages[2].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); assert.equal( call.body.system.some((block: { text?: string }) => block.text === "compact continuation"), false ); - assert.equal(call.body.messages[3].content[0].cache_control, undefined); + assert.deepEqual(call.body.messages[3].content[0].cache_control, { + type: "ephemeral", + ttl: "5m", + }); }); test("chatCore keeps Claude normalization for non-Claude-Code Claude passthrough", async () => { const { call, result } = await invokeChatCore({ @@ -1264,12 +1267,12 @@ test("chatCore preserves cache_control automatically for Claude Code single-mode assert.deepEqual(call.body.system[2].cache_control, { type: "ephemeral", ttl: "5m" }); assert.deepEqual(call.body.messages[0].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); // base.ts executor explicitly strips cache_control from tools for Claude Code clients assert.equal(call.body.tools[0].cache_control, undefined); }); -test("chatCore supplements a missing message cache breakpoint for native Claude Code requests", async () => { +test("chatCore advances a message cache breakpoint for native Claude Code requests", async () => { await settingsDb.updateSettings({ alwaysPreserveClientCache: "auto" }); invalidateCacheControlSettingsCache(); @@ -1294,7 +1297,16 @@ test("chatCore supplements a missing message cache breakpoint for native Claude }, ], messages: [ - { role: "user", content: [{ type: "text", text: "first turn" }] }, + { + role: "user", + content: [ + { + type: "text", + text: "first turn", + cache_control: { type: "ephemeral" }, + }, + ], + }, { role: "assistant", content: [{ type: "text", text: "first response" }] }, { role: "user", content: [{ type: "text", text: "latest turn" }] }, ], @@ -1313,7 +1325,7 @@ test("chatCore supplements a missing message cache breakpoint for native Claude assert.deepEqual(call.body.messages[2].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); assert.equal(call.body.tools[0].cache_control, undefined); }); @@ -1401,10 +1413,12 @@ test("chatCore disables raw Claude passthrough when cache preservation is off an ), true ); - // Cache preservation is on for native Claude, so cache markers are intact + // Cache preservation is on for native Claude, so cache markers are intact. This PR: + // an omitted TTL now defaults to "5m" once a "5m" boundary breakpoint (the system + // block above) has already appeared, instead of always defaulting to "1h". assert.deepEqual(call.body.messages[0].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); // Tools disable flag is applied assert.equal("_disableToolPrefix" in call.body, false); diff --git a/tests/unit/claude-code-parity.test.ts b/tests/unit/claude-code-parity.test.ts index 0e58e9a3f3..07a1e5e2d1 100644 --- a/tests/unit/claude-code-parity.test.ts +++ b/tests/unit/claude-code-parity.test.ts @@ -360,7 +360,7 @@ describe("ensureCacheControlOnLastUserMessage", () => { assert.deepEqual(body.messages[2].content[0].cache_control, { type: "ephemeral" }); }); - it("keeps an existing message breakpoint without adding another", () => { + it("keeps an existing message breakpoint and advances one to the last user message", () => { const body = { messages: [ { @@ -377,9 +377,33 @@ describe("ensureCacheControlOnLastUserMessage", () => { ], }; + ensureCacheControlOnLastUserMessage(body); ensureCacheControlOnLastUserMessage(body); - assert.equal(body.messages[1].content[0].cache_control, undefined); + assert.deepEqual(body.messages[0].content[0].cache_control, { type: "ephemeral" }); + assert.deepEqual(body.messages[1].content[0].cache_control, { type: "ephemeral" }); + assert.equal( + body.messages.flatMap((message) => message.content).filter((block) => block.cache_control) + .length, + 2 + ); + }); + + it("keeps a new tail breakpoint at 5m after an existing 5m breakpoint", () => { + const body = { + system: [ + { type: "text", text: "long", cache_control: { type: "ephemeral", ttl: "1h" } }, + { type: "text", text: "short", cache_control: { type: "ephemeral", ttl: "5m" } }, + ], + messages: [{ role: "user", content: [{ type: "text", text: "Follow up" }] }], + }; + + ensureCacheControlOnLastUserMessage(body); + + assert.deepEqual(body.messages[0].content[0].cache_control, { + type: "ephemeral", + ttl: "5m", + }); }); it("does not exceed four surviving system and message breakpoints", () => { @@ -452,6 +476,25 @@ describe("normalizeCacheControlTtl", () => { }); }); + it("defaults missing ttl to 5m after a 5m breakpoint", () => { + const body = { + system: [{ type: "text", text: "stable", cache_control: { type: "ephemeral", ttl: "5m" } }], + messages: [ + { + role: "user", + content: [{ type: "text", text: "follow up", cache_control: { type: "ephemeral" } }], + }, + ], + }; + + normalizeCacheControlTtl(body); + + assert.deepEqual(body.messages[0].content[0].cache_control, { + type: "ephemeral", + ttl: "5m", + }); + }); + it("leaves blocks without cache_control untouched", () => { const body = { system: [{ type: "text", text: "no cache_control here" }], From 54b39690e57517e77587a078f558bc6b67fadee6 Mon Sep 17 00:00:00 2001 From: stanley Date: Thu, 20 Aug 2026 23:10:58 +0700 Subject: [PATCH 080/135] fix(compression): bound RTK raw-output store growth and make pointer reads O(bucket) (#10659) (#10660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated together with related stanleytejakusuma PRs (typecheck:core clean, complexity/cognitive/file-size/changelog gates green, focused tests passing). Great incident writeup and clean fix. Thanks! --- .../compression/engines/rtk/configSchema.ts | 21 ++ .../services/compression/engines/rtk/index.ts | 23 +- .../compression/engines/rtk/rawOutput.ts | 268 ++++++++++++++++-- open-sse/services/compression/types.ts | 6 + .../rtk-raw-output-retention.test.ts | 111 ++++++++ 5 files changed, 408 insertions(+), 21 deletions(-) create mode 100644 tests/unit/compression/rtk-raw-output-retention.test.ts diff --git a/open-sse/services/compression/engines/rtk/configSchema.ts b/open-sse/services/compression/engines/rtk/configSchema.ts index e7699120e3..2af6385015 100644 --- a/open-sse/services/compression/engines/rtk/configSchema.ts +++ b/open-sse/services/compression/engines/rtk/configSchema.ts @@ -66,6 +66,22 @@ export const RTK_SCHEMA: EngineConfigField[] = [ { value: "always", label: "always" }, ], }, + { + key: "rawOutputMaxFiles", + type: "number", + label: "Max raw-output files (oldest purged beyond this)", + defaultValue: DEFAULT_RTK_CONFIG.rawOutputMaxFiles, + min: 1, + max: 10_000_000, + }, + { + key: "rawOutputMaxAgeDays", + type: "number", + label: "Max raw-output age (days)", + defaultValue: DEFAULT_RTK_CONFIG.rawOutputMaxAgeDays, + min: 1, + max: 3650, + }, { key: "enableRenderers", type: "boolean", @@ -113,5 +129,10 @@ export function validateRtkEngineConfig(config: Record): Engine ) { errors.push("rawOutputRetention must be never, failures, or always"); } + for (const key of ["rawOutputMaxFiles", "rawOutputMaxAgeDays"]) { + if (config[key] !== undefined && (typeof config[key] !== "number" || config[key] < 1)) { + errors.push(`${key} must be a positive number`); + } + } return { valid: errors.length === 0, errors }; } diff --git a/open-sse/services/compression/engines/rtk/index.ts b/open-sse/services/compression/engines/rtk/index.ts index 0d34980043..33b6c90b59 100644 --- a/open-sse/services/compression/engines/rtk/index.ts +++ b/open-sse/services/compression/engines/rtk/index.ts @@ -9,7 +9,11 @@ import { matchRtkFilter } from "./filterLoader.ts"; import { applyLineFilter } from "./lineFilter.ts"; import { smartTruncate } from "./smartTruncate.ts"; import { normalizeCodeLanguage, stripCode } from "./codeStripper.ts"; -import { maybePersistRtkRawOutput, type RtkRawOutputPointer } from "./rawOutput.ts"; +import { + maybePersistRtkRawOutput, + scheduleRtkRawOutputPurge, + type RtkRawOutputPointer, +} from "./rawOutput.ts"; import { applyRenderer } from "./renderers/index.ts"; import { isTextBlock } from "../../messageContent.ts"; import { adaptBodyForCompression } from "../../bodyAdapter.ts"; @@ -121,6 +125,15 @@ function mergeRtkConfig(base?: Partial, override?: Record//...`) so reads are O(bucket), + * and a bounded async purge (see purgeRtkRawOutput) caps total files/age. + */ +const RAW_OUTPUT_BUCKET_LEN = 2; +/** Legacy flat-store entries beyond this size are not synchronously scanned (freeze guard). */ +const LEGACY_FLAT_SCAN_GUARD = 100_000; + +function rawOutputDir(): string { + return path.join(dataDir(), "rtk", "raw-output"); +} + +function bucketDir(id: string): string { + return path.join(rawOutputDir(), id.slice(0, RAW_OUTPUT_BUCKET_LEN)); +} + export function maybePersistRtkRawOutput( raw: string, options: { @@ -93,8 +112,9 @@ export function maybePersistRtkRawOutput( .replace(/^_+|_+$/g, "") .slice(0, 48); const id = safeId(`${now}:${commandSlug}:${raw.length}:${redaction.text}`); - const dir = path.join(dataDir(), "rtk", "raw-output"); - const filePath = path.join(dir, `${now}-${commandSlug || "tool-output"}-${id}.log`); + const dir = bucketDir(id); + const fileName = `${now}-${commandSlug || "tool-output"}-${id}.log`; + const filePath = path.join(dir, fileName); try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, redaction.text); @@ -135,11 +155,33 @@ export function maybePersistRtkRawOutput( } export function readRtkRawOutput(pointerId: string): string | null { - const dir = path.join(dataDir(), "rtk", "raw-output"); + const dir = rawOutputDir(); if (!fs.existsSync(dir)) return null; - const entry = fs - .readdirSync(dir) - .find((file) => file.endsWith(".log") && file.includes(pointerId)); + + // Bucketed layout first (new writes): one tiny subdir read instead of a full-store scan. + const bucket = bucketDir(pointerId); + if (fs.existsSync(bucket)) { + const entry = fs + .readdirSync(bucket) + .find((file) => file.endsWith(".log") && file.includes(pointerId)); + if (entry) { + const fullPath = path.join(bucket, entry); + if (!fullPath.startsWith(dir)) return null; + return fs.readFileSync(fullPath, "utf8"); + } + } + + // Legacy flat layout (pre-bucket writes). Guarded: scanning a multi-million-entry flat + // store synchronously is exactly the event-loop freeze #10659 reports, so refuse once + // the flat store is pathologically large instead of stalling the gateway. + const entries = fs.readdirSync(dir); + if (entries.length > LEGACY_FLAT_SCAN_GUARD) { + console.warn( + `[rtk-raw-output] legacy flat store has ${entries.length} entries; skipping O(n) pointer scan for ${pointerId}` + ); + return null; + } + const entry = entries.find((file) => file.endsWith(".log") && file.includes(pointerId)); if (!entry) return null; const fullPath = path.join(dir, entry); if (!fullPath.startsWith(dir)) return null; @@ -156,6 +198,51 @@ function commandFromSlug(fileName: string): string { return slug.replace(/_+/g, " ").trim(); } +/** + * Collect every `.log` path in the store (legacy flat + buckets). The flat store is + * guarded so a pathological legacy directory cannot freeze the loop; bucket dirs are + * small by construction (the purge cap keeps each bucket bounded). + */ +function collectRawOutputLogFiles(dir: string): Array<{ name: string; fullPath: string }> { + const logs: Array<{ name: string; fullPath: string }> = []; + let entries: string[]; + try { + entries = fs.readdirSync(dir); + } catch { + return logs; + } + if (entries.length <= LEGACY_FLAT_SCAN_GUARD) { + for (const entry of entries) { + if (entry.endsWith(".log")) logs.push({ name: entry, fullPath: path.join(dir, entry) }); + } + } else { + console.warn( + `[rtk-raw-output] legacy flat store has ${entries.length} entries; skipping sample scan this run` + ); + } + for (const entry of entries) { + if (entry.length !== RAW_OUTPUT_BUCKET_LEN) continue; + const subPath = path.join(dir, entry); + let isDir = false; + try { + isDir = fs.statSync(subPath).isDirectory(); + } catch { + continue; + } + if (!isDir) continue; + let subEntries: string[]; + try { + subEntries = fs.readdirSync(subPath); + } catch { + continue; + } + for (const name of subEntries) { + if (name.endsWith(".log")) logs.push({ name, fullPath: path.join(subPath, name) }); + } + } + return logs; +} + /** * Read the opt-in RTK raw-output store (`DATA_DIR/rtk/raw-output/*.log`) into * `CommandSample[]` for the pure miners `discoverRepeatedNoise()` / `suggestFilter()`. @@ -166,24 +253,17 @@ function commandFromSlug(fileName: string): string { * memory. No throw: a corrupt entry is dropped, not propagated. */ export function listRtkCommandSamples(opts: { limit?: number } = {}): CommandSample[] { - const dir = path.join(dataDir(), "rtk", "raw-output"); + const dir = rawOutputDir(); if (!fs.existsSync(dir)) return []; const limit = Math.max(1, Math.floor(opts.limit ?? 500)); - let logs: string[]; - try { - logs = fs.readdirSync(dir).filter((f) => f.endsWith(".log")); - } catch { - return []; - } + const logs = collectRawOutputLogFiles(dir); // Newest first: the filename is timestamp-prefixed, so a reverse lexical sort works. - logs.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0)); + logs.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0)); const samples: CommandSample[] = []; - for (const fileName of logs) { + for (const { name, fullPath } of logs) { if (samples.length >= limit) break; - const fullPath = path.join(dir, fileName); - if (!fullPath.startsWith(dir)) continue; let output: string; try { output = fs.readFileSync(fullPath, "utf8"); @@ -191,7 +271,6 @@ export function listRtkCommandSamples(opts: { limit?: number } = {}): CommandSam continue; } if (output.trim().length === 0) continue; - let command = ""; try { const metaRaw = fs.readFileSync(fullPath.replace(/\.log$/, ".meta.json"), "utf8"); @@ -200,9 +279,158 @@ export function listRtkCommandSamples(opts: { limit?: number } = {}): CommandSam } catch { // No/!invalid sidecar → fall back to the filename slug below. } - if (!command) command = commandFromSlug(fileName) || "tool-output"; - + if (!command) command = commandFromSlug(name) || "tool-output"; samples.push({ command, output }); } return samples; } + +export interface RtkRawOutputPurgeOptions { + maxAgeDays?: number; + maxFiles?: number; +} + +export interface RtkRawOutputPurgeResult { + skipped: boolean; + scanned: number; + deleted: number; + errors: number; +} + +const PURGE_THROTTLE_MS = 60_000; +let lastRawOutputPurgeAt = 0; + +/** Test hook: clear the purge throttle so a test can exercise two consecutive purges. */ +export function resetRtkRawOutputPurgeThrottle(): void { + lastRawOutputPurgeAt = 0; +} + +async function mapLimit( + items: T[], + limit: number, + fn: (item: T) => Promise +): Promise { + let index = 0; + const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { + while (index < items.length) { + const item = items[index++]; + await fn(item); + } + }); + await Promise.all(workers); +} + +/** + * #10659: bounded retention for the raw-output store. Enforces max age and max file count + * asynchronously (never blocks the event loop), best-effort (never throws into callers), + * and throttled to once per minute from the scheduler. + * + * The legacy flat store is skipped when it is pathologically large (guard) — scanning it + * synchronously/async with millions of entries is what froze gateways; the operator does + * a one-off cleanup and the bucketized layout keeps new growth bounded. + */ +export async function purgeRtkRawOutput( + opts: RtkRawOutputPurgeOptions = {} +): Promise { + const now = Date.now(); + if (now - lastRawOutputPurgeAt < PURGE_THROTTLE_MS) { + return { skipped: true, scanned: 0, deleted: 0, errors: 0 }; + } + lastRawOutputPurgeAt = now; + + const maxAgeDays = Math.max(1, Math.floor(opts.maxAgeDays ?? 30)); + const maxFiles = Math.max(1, Math.floor(opts.maxFiles ?? 100_000)); + const maxAgeMs = maxAgeDays * 86_400_000; + const dir = rawOutputDir(); + const result: RtkRawOutputPurgeResult = { skipped: false, scanned: 0, deleted: 0, errors: 0 }; + if (!fs.existsSync(dir)) return result; + + try { + const candidates: Array<{ file: string; meta: string | null; ts: number }> = []; + const flat = await fsp.readdir(dir); + if (flat.length > LEGACY_FLAT_SCAN_GUARD) { + console.warn( + `[rtk-raw-output] legacy flat store has ${flat.length} entries; purge skips flat scan this run (one-off manual cleanup recommended)` + ); + } else { + for (const name of flat) { + if (!name.endsWith(".log")) continue; + candidates.push({ + file: path.join(dir, name), + meta: path.join(dir, name.replace(/\.log$/, ".meta.json")), + ts: parseInt(name, 10) || 0, + }); + } + } + for (const entry of flat) { + if (entry.length !== RAW_OUTPUT_BUCKET_LEN) continue; + const subPath = path.join(dir, entry); + let isDir = false; + try { + isDir = (await fsp.stat(subPath)).isDirectory(); + } catch { + continue; + } + if (!isDir) continue; + let subEntries: string[]; + try { + subEntries = await fsp.readdir(subPath); + } catch { + continue; + } + for (const name of subEntries) { + if (!name.endsWith(".log")) continue; + candidates.push({ + file: path.join(subPath, name), + meta: path.join(subPath, name.replace(/\.log$/, ".meta.json")), + ts: parseInt(name, 10) || 0, + }); + } + } + result.scanned = candidates.length; + + const agedOut = candidates.filter((c) => c.ts > 0 && now - c.ts > maxAgeMs); + const remaining = candidates.filter((c) => !agedOut.includes(c)); + remaining.sort((a, b) => b.ts - a.ts || (a.file < b.file ? 1 : -1)); + const keep = new Set(remaining.slice(0, maxFiles).map((c) => c.file)); + const overflow = remaining.filter((c) => !keep.has(c.file)); + + await mapLimit([...agedOut, ...overflow], 32, async (c) => { + try { + await fsp.unlink(c.file); + result.deleted++; + } catch { + result.errors++; + } + if (c.meta) { + try { + await fsp.unlink(c.meta); + } catch { + // Missing/never-written sidecar is fine. + } + } + }); + + if (result.deleted > 0 || result.errors > 0) { + console.log( + `[rtk-raw-output] purge: scanned=${result.scanned} deleted=${result.deleted} errors=${result.errors} (maxFiles=${maxFiles}, maxAgeDays=${maxAgeDays})` + ); + } + } catch (err) { + console.warn("[rtk-raw-output] purge failed:", (err as Error).message); + result.errors++; + } + return result; +} + +/** + * Schedule a throttled best-effort purge off the hot path. Safe to call on every write: + * purgeRtkRawOutput itself throttles to once per minute. + */ +export function scheduleRtkRawOutputPurge(opts: RtkRawOutputPurgeOptions = {}): void { + setImmediate(() => { + void purgeRtkRawOutput(opts).catch(() => { + /* best-effort */ + }); + }); +} diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index f35dfac37f..70d457aa91 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -103,6 +103,10 @@ export interface RtkConfig { trustProjectFilters: boolean; rawOutputRetention: RtkRawOutputRetention; rawOutputMaxBytes: number; + /** #10659: cap on total raw-output files before the oldest are purged. Default: 100_000. */ + rawOutputMaxFiles?: number; + /** #10659: max age (days) of retained raw-output files. Default: 30. */ + rawOutputMaxAgeDays?: number; /** R5: enable grouping of near-equivalent consecutive lines. Default: false. */ enableGrouping?: boolean; /** R5: minimum consecutive similar-line run to trigger grouping. Default: 3. */ @@ -473,6 +477,8 @@ export const DEFAULT_RTK_CONFIG: RtkConfig = { trustProjectFilters: false, rawOutputRetention: "never", rawOutputMaxBytes: 1_048_576, + rawOutputMaxFiles: 100_000, + rawOutputMaxAgeDays: 30, enableGrouping: false, groupingThreshold: 3, stripCodeComments: false, diff --git a/tests/unit/compression/rtk-raw-output-retention.test.ts b/tests/unit/compression/rtk-raw-output-retention.test.ts new file mode 100644 index 0000000000..32349bcafb --- /dev/null +++ b/tests/unit/compression/rtk-raw-output-retention.test.ts @@ -0,0 +1,111 @@ +import { describe, it, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + maybePersistRtkRawOutput, + purgeRtkRawOutput, + readRtkRawOutput, + resetRtkRawOutputPurgeThrottle, +} from "../../../open-sse/services/compression/engines/rtk/rawOutput.ts"; + +const originalDataDir = process.env.DATA_DIR; + +afterEach(() => { + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; + resetRtkRawOutputPurgeThrottle(); +}); + +function freshDataDir(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rtk-store-")); + process.env.DATA_DIR = dir; + return dir; +} + +function storeRoot(dataDir: string): string { + return path.join(dataDir, "rtk", "raw-output"); +} + +/** Write a raw-output file in the bucketized layout and return its pointer id. */ +function writeBucketFile( + dataDir: string, + ts: number, + command: string, + idHex: string, + content: string +): string { + const bucket = path.join(storeRoot(dataDir), idHex.slice(0, 2)); + fs.mkdirSync(bucket, { recursive: true }); + const slug = command.replace(/[^A-Za-z0-9_-]+/g, "_").slice(0, 48); + fs.writeFileSync(path.join(bucket, `${ts}-${slug}-${idHex}.log`), content); + return idHex; +} + +describe("RTK raw-output bounded retention (#10659)", () => { + it("writes new captures into id-prefix buckets and reads them back", () => { + const dataDir = freshDataDir(); + const text = "error: boom\nnoise\n".repeat(8); + const pointer = maybePersistRtkRawOutput(text, { retention: "always" }); + assert.ok(pointer, "pointer should be produced with retention=always"); + // Bucketed layout: pointer.path sits one level below the store root. + assert.equal(path.dirname(pointer!.path), path.join(storeRoot(dataDir), pointer!.id.slice(0, 2))); + assert.ok(fs.existsSync(pointer!.path), "bucket file exists on disk"); + assert.equal(readRtkRawOutput(pointer!.id), text, "read resolves via bucket lookup"); + }); + + it("still reads legacy flat-store files (backward compatibility)", () => { + const dataDir = freshDataDir(); + const store = storeRoot(dataDir); + fs.mkdirSync(store, { recursive: true }); + const id = "ab".padEnd(24, "0"); + fs.writeFileSync(path.join(store, `1710000000000-tool-output-${id}.log`), "legacy content"); + assert.equal(readRtkRawOutput(id), "legacy content"); + }); + + it("returns null for unknown pointer ids", () => { + freshDataDir(); + assert.equal(readRtkRawOutput("ffffffffffffffffffffffff"), null); + }); + + it("purge deletes files older than maxAgeDays and keeps recent ones", async () => { + const dataDir = freshDataDir(); + const now = Date.now(); + const oldId = writeBucketFile(dataDir, now - 40 * 86_400_000, "old", "aa".padEnd(24, "0"), "old"); + writeBucketFile(dataDir, now - 40 * 86_400_000, "old2", "ab".padEnd(24, "0"), "old2"); + const recentId = writeBucketFile(dataDir, now - 1000, "recent", "ac".padEnd(24, "0"), "recent"); + + const result = await purgeRtkRawOutput({ maxAgeDays: 30, maxFiles: 100_000 }); + assert.equal(result.skipped, false); + assert.equal(result.deleted, 2); + assert.equal(readRtkRawOutput(oldId), null, "aged-out file purged"); + assert.equal(readRtkRawOutput(recentId), "recent", "recent file kept"); + }); + + it("purge caps the store at maxFiles, keeping the newest", async () => { + const dataDir = freshDataDir(); + const now = Date.now(); + const ids: string[] = []; + for (let i = 0; i < 8; i++) { + const id = `b${i}`.padEnd(24, "b").slice(0, 24); + writeBucketFile(dataDir, now - i * 1000, `cmd${i}`, id, `content${i}`); + ids.push(id); + } + const result = await purgeRtkRawOutput({ maxAgeDays: 30, maxFiles: 5 }); + assert.equal(result.deleted, 3); + // Newest 5 (i=0..4) survive; oldest 3 (i=5..7) are purged. + assert.equal(readRtkRawOutput(ids[0]), "content0"); + assert.equal(readRtkRawOutput(ids[4]), "content4"); + assert.equal(readRtkRawOutput(ids[5]), null); + assert.equal(readRtkRawOutput(ids[7]), null); + }); + + it("retention=never writes nothing to disk", () => { + const dataDir = freshDataDir(); + const pointer = maybePersistRtkRawOutput("some output", { retention: "never" }); + assert.equal(pointer, null); + assert.equal(fs.existsSync(storeRoot(dataDir)), false); + }); +}); From b59a88b7eb2fcadde96ff6974337c8dc7138fd30 Mon Sep 17 00:00:00 2001 From: stanley Date: Thu, 20 Aug 2026 23:11:03 +0700 Subject: [PATCH 081/135] fix(pricing): 3 dead entries in LITELLM_PROVIDER_MAP silently drop synced pricing (#10636) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (28/28 focused pricing-sync tests, gates green). Excellent systematic audit of the whole alias map, not just the one you hit. Thanks! --- src/lib/pricingSync.ts | 23 +++++++++++++++++++---- tests/unit/pricing-sync.test.ts | 11 ++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/lib/pricingSync.ts b/src/lib/pricingSync.ts index a334a9f096..e828d962ed 100644 --- a/src/lib/pricingSync.ts +++ b/src/lib/pricingSync.ts @@ -105,10 +105,22 @@ const LITELLM_PROVIDER_MAP: Record = { vertex_ai: ["gemini"], "vertex_ai-anthropic_models": ["anthropic"], google: ["gemini"], - deepseek: ["if"], + // Registry ALIAS, not registry id — pricingSync writes/reads are keyed by + // alias everywhere else (see getPricingForModel(provider, model) callers). + // Four of these previously used the provider's `id` string, which is not a + // valid pricing-lookup key for that provider and, worse, for `deepseek` a + // real (but wrong) alias existed under that string — silently routing + // DeepSeek's synced pricing onto Qoder (open-sse/config/providers/registry/ + // qoder/index.ts, alias "if", an unrelated third-party API) instead of + // DeepSeek (alias "ds"). `bedrock`/`bedrock_converse` and `cloudflare` + // pointed at their provider's `id` ("kiro", "cloudflare-ai") rather than + // its `alias` ("kr", "cf") — not wrong-provider, just a dead key nothing + // downstream ever looks up, so those two providers silently never received + // synced pricing at all. + deepseek: ["ds"], groq: ["groq"], together_ai: ["openrouter"], - bedrock: ["kiro"], + bedrock: ["kr"], fireworks_ai: ["fireworks"], cerebras: ["cerebras"], nvidia_nim: ["nvidia"], @@ -116,8 +128,11 @@ const LITELLM_PROVIDER_MAP: Record = { "vertex_ai-language_models": ["gemini"], "vertex_ai-mistral_models": ["mistral"], gemini: ["gemini"], - bedrock_converse: ["kiro"], - cloudflare: ["cloudflare-ai"], + bedrock_converse: ["kr"], + cloudflare: ["cf"], + // stability-ai has no chat-completions registry entry (image-only: + // open-sse/config/providers/registry/stability-ai/imageModels.ts) — left + // as-is rather than guessed at; not the same bug shape as the three above. stability: ["stability-ai"], }; diff --git a/tests/unit/pricing-sync.test.ts b/tests/unit/pricing-sync.test.ts index 2185c8007f..29ebc0ec41 100644 --- a/tests/unit/pricing-sync.test.ts +++ b/tests/unit/pricing-sync.test.ts @@ -138,9 +138,14 @@ describe("transformToOmniRoute", () => { const result = transformToOmniRoute(raw); - // deepseek maps to "if" alias - assert.ok(result.if, "Should map deepseek to if alias"); - assert.ok(result.if["deepseek-chat"]); + // deepseek maps to "ds" (its real registry alias — open-sse/config/providers/ + // registry/deepseek/index.ts). Previously mapped to "if" (Qoder's alias, an + // unrelated provider) — fixed alongside the other dead LITELLM_PROVIDER_MAP + // entries (bedrock/bedrock_converse/cloudflare) that pointed at a provider's + // `id` instead of its `alias`. + assert.ok(result.ds, "Should map deepseek to its real ds alias"); + assert.ok(result.ds["deepseek-chat"]); + assert.ok(!result.if, "Must not route deepseek pricing onto Qoder's if alias"); }); test("skips entries without input cost", () => { From 998c3c2129b1aa8fb16a43e7d80d27fd23148c45 Mon Sep 17 00:00:00 2001 From: stanley Date: Thu, 20 Aug 2026 23:11:07 +0700 Subject: [PATCH 082/135] fix(pricing): DeepSeek V4 static defaults stale by 4 days, off by ~1.6-2.4x (#10635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (28/28 focused pricing tests, gates green). Appreciate the conservative off-peak-only scope and the live verification against the pricing page. Thanks! --- src/shared/constants/pricing/frontier-labs.ts | 30 ++++++++----- ...cing-deepseek-v4-static-regression.test.ts | 44 +++++++++++++++++++ 2 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 tests/unit/pricing-deepseek-v4-static-regression.test.ts diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index 2923945b9c..f1bb039a53 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -317,20 +317,28 @@ export const DEFAULT_PRICING_FRONTIER = { reasoning: 2.19, cache_creation: 0.55, }, - // DeepSeek official API list prices, checked 2026-08-13. + // DeepSeek official API list prices, checked 2026-08-18. Superseded the + // prior 2026-08-13 flat prices below: DeepSeek switched v4-pro/v4-flash to + // peak/off-peak dynamic pricing on 2026-08-17 (peak = exactly 2x off-peak; + // peak hours 01:00-04:00 and 06:00-10:00 UTC — see + // https://api-docs.deepseek.com/quick_start/pricing/). This static table has + // no time-of-day dimension, so these are the OFF-PEAK (lower-bound) prices — + // a deliberate, documented undercount during the two peak windows, never an + // overcount. True peak-awareness would need a time dimension threaded through + // getPricingForModel() and every call site; out of scope for this fix. "deepseek-v4-pro": { - input: 0.435, - output: 0.87, - cached: 0.003625, - reasoning: 0.87, - cache_creation: 0.435, + input: 0.66, + output: 1.98, + cached: 0.022, + reasoning: 1.98, + cache_creation: 0.66, }, "deepseek-v4-flash": { - input: 0.14, - output: 0.28, - cached: 0.0028, - reasoning: 0.28, - cache_creation: 0.14, + input: 0.22, + output: 0.66, + cached: 0.007, + reasoning: 0.66, + cache_creation: 0.22, }, }, blackbox: { diff --git a/tests/unit/pricing-deepseek-v4-static-regression.test.ts b/tests/unit/pricing-deepseek-v4-static-regression.test.ts new file mode 100644 index 0000000000..e5054652b1 --- /dev/null +++ b/tests/unit/pricing-deepseek-v4-static-regression.test.ts @@ -0,0 +1,44 @@ +// Regression guard for the corrected DeepSeek V4 static pricing defaults (#10635): DeepSeek +// switched deepseek-v4-pro/deepseek-v4-flash to peak/off-peak dynamic pricing on 2026-08-17; +// the static table carries the off-peak (lower-bound) values. Asserts the exact corrected +// figures and the documented output=3x-input / peak=2x-off-peak internal consistency so a +// future accidental revert to the stale 2026-08-13 numbers is caught by CI. +import { test } from "node:test"; +import assert from "node:assert/strict"; + +const { DEFAULT_PRICING_FRONTIER } = await import( + "../../src/shared/constants/pricing/frontier-labs.ts" +); + +test("deepseek-v4-pro carries the corrected off-peak static prices", () => { + const entry = (DEFAULT_PRICING_FRONTIER as Record>>) + .deepseek["deepseek-v4-pro"]; + assert.ok(entry, "deepseek-v4-pro entry must exist under DEFAULT_PRICING_FRONTIER.deepseek"); + assert.equal(entry.input, 0.66); + assert.equal(entry.output, 1.98); + assert.equal(entry.cached, 0.022); + assert.equal(entry.reasoning, 1.98); + assert.equal(entry.cache_creation, 0.66); +}); + +test("deepseek-v4-flash carries the corrected off-peak static prices", () => { + const entry = (DEFAULT_PRICING_FRONTIER as Record>>) + .deepseek["deepseek-v4-flash"]; + assert.ok(entry, "deepseek-v4-flash entry must exist under DEFAULT_PRICING_FRONTIER.deepseek"); + assert.equal(entry.input, 0.22); + assert.equal(entry.output, 0.66); + assert.equal(entry.cached, 0.007); + assert.equal(entry.reasoning, 0.66); + assert.equal(entry.cache_creation, 0.22); +}); + +test("deepseek-v4 entries keep DeepSeek's documented output=3x-input ratio", () => { + const deepseek = (DEFAULT_PRICING_FRONTIER as Record>>) + .deepseek; + for (const model of ["deepseek-v4-pro", "deepseek-v4-flash"]) { + const entry = deepseek[model]; + assert.equal(entry.output, entry.input * 3, `${model}: output should be 3x input`); + assert.equal(entry.reasoning, entry.output, `${model}: reasoning should match output`); + assert.ok(entry.cached < entry.input, `${model}: cached must be cheaper than input`); + } +}); From 01b3828278c1138b969fc2c8209acda878db706e Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Fri, 21 Aug 2026 00:21:03 +0800 Subject: [PATCH 083/135] fix(sse): strip corrupted request_id from upstream SSE responses (#10223) (#10666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated together with related HouMinXi PRs (22/22 focused tests, gates green). Note: the PR description text looks pasted from a different change — the actual diff (corrupted request_id strip, #10223) is what was reviewed and merged. Thanks! --- open-sse/transformer/responsesTransformer.ts | 24 ++++++ ...s-transformer-corrupted-request-id.test.ts | 83 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/unit/responses-transformer-corrupted-request-id.test.ts diff --git a/open-sse/transformer/responsesTransformer.ts b/open-sse/transformer/responsesTransformer.ts index 1ef35e4aa6..f22f38f9ed 100644 --- a/open-sse/transformer/responsesTransformer.ts +++ b/open-sse/transformer/responsesTransformer.ts @@ -7,6 +7,15 @@ import { } from "../utils/reasoningPlaceholder.ts"; import * as fs from "fs"; import * as path from "path"; + +// #10223: threshold for detecting corrupted request_id fields. Normal +// request IDs are <100 chars. DeepSeek's SSE encoder bug produces 200+ +// char values with response-ID fragments. The 100-char gap between normal +// (<100) and threshold (200) provides safety margin for providers that +// use moderately longer IDs. The transformer never reads request_id, so +// stripping it has no functional impact on the output. +const CORRUPTED_REQUEST_ID_THRESHOLD = 200; + /** * Responses API Transformer * Converts OpenAI Chat Completions SSE to Codex Responses API SSE format @@ -605,6 +614,21 @@ export function createResponsesApiTransformStream( continue; } + // #10223: strip request_id when it looks corrupted (suspiciously + // long — normal request IDs are <100 chars). Some providers + // (DeepSeek) have SSE encoder bugs that leak response-ID fragments + // into this field, producing 200+ char values. Well-behaved + // providers' request_id is preserved. + if ( + typeof parsed.request_id === "string" && + parsed.request_id.length >= CORRUPTED_REQUEST_ID_THRESHOLD + ) { + logger?.logInput( + `[ResponsesTransformer] stripped corrupted request_id (${parsed.request_id.length} chars)` + ); + delete parsed.request_id; + } + if (parsed.usage) { state.usage = normalizeResponsesUsage(state.usage, parsed.usage); } diff --git a/tests/unit/responses-transformer-corrupted-request-id.test.ts b/tests/unit/responses-transformer-corrupted-request-id.test.ts new file mode 100644 index 0000000000..c952527b6d --- /dev/null +++ b/tests/unit/responses-transformer-corrupted-request-id.test.ts @@ -0,0 +1,83 @@ +// Regression guard for #10223: DeepSeek's SSE encoder bug leaks response-ID +// fragments into `request_id`, producing suspiciously long (200+ char) values. +// The transformer never reads `request_id` for its own output, but it must +// strip a corrupted one (logging that it did) and must NOT touch a normal, +// well-behaved provider's request_id. +import test from "node:test"; +import assert from "node:assert/strict"; + +const { createResponsesApiTransformStream } = await import( + "../../open-sse/transformer/responsesTransformer.ts" +); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +async function runTransformStream(chunks, logger = null) { + const stream = createResponsesApiTransformStream(logger, 3000, {}); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + + const output = []; + const readerTask = (async () => { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + output.push(decoder.decode(value)); + } + })(); + + for (const chunk of chunks) { + await writer.write(encoder.encode(chunk)); + } + await writer.close(); + await readerTask; + + return output.join(""); +} + +function makeMockLogger() { + const inputs = []; + return { + inputs, + logInput: (event) => inputs.push(event), + logOutput: () => {}, + flush: () => {}, + }; +} + +test("BUG #10223: a corrupted (>=200 char) request_id is stripped and logged", async () => { + const corruptedId = "r".repeat(250); + const logger = makeMockLogger(); + + await runTransformStream( + [ + `data: {"id":"chatcmpl_1","request_id":"${corruptedId}","choices":[{"index":0,"delta":{"content":"Hi"}}]}\n\n`, + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + ], + logger + ); + + const stripLog = logger.inputs.find( + (entry) => typeof entry === "string" && entry.includes("stripped corrupted request_id") + ); + assert.ok(stripLog, "logger.logInput should be called noting the corrupted request_id was stripped"); + assert.match(stripLog, /\(250 chars\)/); +}); + +test("a normal (<200 char) request_id is left untouched — no strip logged", async () => { + const logger = makeMockLogger(); + + await runTransformStream( + [ + 'data: {"id":"chatcmpl_1","request_id":"req_normal_12345","choices":[{"index":0,"delta":{"content":"Hi"}}]}\n\n', + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + ], + logger + ); + + const stripLog = logger.inputs.find( + (entry) => typeof entry === "string" && entry.includes("stripped corrupted request_id") + ); + assert.equal(stripLog, undefined, "a normal-length request_id must never be stripped"); +}); From 424b950856a02aca84a31dcdb951f105e4042426 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Fri, 21 Aug 2026 00:21:07 +0800 Subject: [PATCH 084/135] fix(api): classify OAuth probe timeout as network_error (#10663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (focused tests green, gates green). Clean, minimal classification fix. Thanks! --- src/app/api/providers/[id]/test/route.ts | 1 + ...ction-test-timed-out-network-error.test.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/unit/connection-test-timed-out-network-error.test.ts diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index f8afbb6cb0..36effb38a6 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -153,6 +153,7 @@ export function classifyFailure({ normalized.includes("fetch failed") || normalized.includes("network") || normalized.includes("timeout") || + normalized.includes("timed out") || normalized.includes("econn") || normalized.includes("enotfound") || normalized.includes("socket") diff --git a/tests/unit/connection-test-timed-out-network-error.test.ts b/tests/unit/connection-test-timed-out-network-error.test.ts new file mode 100644 index 0000000000..fbba8ba0b4 --- /dev/null +++ b/tests/unit/connection-test-timed-out-network-error.test.ts @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { classifyFailure } = await import("../../src/app/api/providers/[id]/test/route.ts"); + +// The OAuth probe's own abort message is "Test timed out after Xs" (route.ts's +// AbortSignal.timeout handling), which contains "timed out" but not "timeout" — +// classifyFailure must classify it as network_error, not the generic upstream_error +// fallback, so a transient probe timeout doesn't paint the connection permanently red. +test("classifyFailure maps an OAuth-probe 'timed out' message to network_error", () => { + const diagnosis = classifyFailure({ + error: "Test timed out after 30s", + statusCode: null, + provider: "some-oauth-provider", + }); + + assert.equal(diagnosis.type, "network_error"); + assert.equal(diagnosis.code, "network_error"); +}); + +test("classifyFailure still maps the existing 'timeout' substring to network_error", () => { + const diagnosis = classifyFailure({ + error: "connect ETIMEDOUT — timeout while probing upstream", + statusCode: null, + provider: "some-oauth-provider", + }); + + assert.equal(diagnosis.type, "network_error"); +}); From f52fa9dc853c8cf488351981a2a16221fc1288c2 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Fri, 21 Aug 2026 00:21:11 +0800 Subject: [PATCH 085/135] fix(embeddings): cool down account on hard errors (402/401/5xx) (#10529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (focused embedding-cooldown tests green, gates green). Good parity with the chat path's existing pattern. Thanks! --- config/quality/eslint-suppressions.json | 5 - src/lib/embeddings/service.ts | 42 +++- .../embedding-account-cooldown-10347.test.ts | 95 +++++++++ ...bedding-cooldown-integration-10347.test.ts | 184 ++++++++++++++++++ 4 files changed, 314 insertions(+), 12 deletions(-) create mode 100644 tests/unit/embedding-account-cooldown-10347.test.ts create mode 100644 tests/unit/embedding-cooldown-integration-10347.test.ts diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 81775983bd..40f74e61f9 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -1073,11 +1073,6 @@ "count": 1 } }, - "src/lib/embeddings/service.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "src/lib/evals/runtime.ts": { "no-restricted-imports": { "count": 1 diff --git a/src/lib/embeddings/service.ts b/src/lib/embeddings/service.ts index dab9523ef6..e7337f49e2 100644 --- a/src/lib/embeddings/service.ts +++ b/src/lib/embeddings/service.ts @@ -10,13 +10,14 @@ import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/er import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import * as log from "@/sse/utils/logger"; import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; -import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; import { - getCachedProviderNodes, - getComboByName, - getCombos, - getDatabaseSettings, -} from "@/lib/localDb"; + getProviderCredentials, + clearRecoveredProviderState, + markAccountUnavailable, +} from "@/sse/services/auth"; +import { getCachedProviderNodes } from "@/lib/db/readCache"; +import { getComboByName, getCombos } from "@/lib/db/combos"; +import { getDatabaseSettings } from "@/lib/db/databaseSettings"; import { resolveProxyForConnection } from "@/lib/db/settings"; import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts"; import { handleComboChat } from "@omniroute/open-sse/services/combo.ts"; @@ -309,7 +310,7 @@ export async function createEmbeddingResponse( // #10347 — thread the selected connection id so handleEmbedding can cool the // account on a hard upstream failure (previously always null on /v1/embeddings). connectionId: - ((credentials as { connectionId?: string } | null)?.connectionId) || + (credentials as { connectionId?: string } | null)?.connectionId || options.connectionId || connectionIdForProxy || null, @@ -340,6 +341,33 @@ export async function createEmbeddingResponse( }); } + // #10347: cool down the account on hard errors (402 subscription expired, + // 401 revoked, 403 forbidden, 404 model gone, 429 rate limit, 5xx server + // errors) so the next embedding request skips this account. Mirrors chat.ts + // behavior. + // Skip for 400 (bad request) — the account is fine, the request was wrong. + // Best-effort: don't block the error response on the DB write. + const HARD_ERROR_STATUSES = new Set([401, 402, 403, 404, 429, 500, 502, 503, 504]); + if ( + credentials && + "connectionId" in credentials && + typeof credentials.connectionId === "string" && + HARD_ERROR_STATUSES.has(result.status) + ) { + markAccountUnavailable( + credentials.connectionId, + result.status, + result.error || "Embedding provider error", + provider, + resolvedModel || null + ).catch((err) => { + log.debug( + "EMBED", + `Cooldown write failed for ${provider}/${credentials.connectionId?.slice(0, 8)}: ${err}` + ); + }); + } + responseHeaders.set("Content-Type", "application/json"); const errorPayload = toJsonErrorPayload(result.error, "Embedding provider error"); return new Response(JSON.stringify(errorPayload), { diff --git a/tests/unit/embedding-account-cooldown-10347.test.ts b/tests/unit/embedding-account-cooldown-10347.test.ts new file mode 100644 index 0000000000..a9c1d8b5ce --- /dev/null +++ b/tests/unit/embedding-account-cooldown-10347.test.ts @@ -0,0 +1,95 @@ +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-embed-cooldown-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "embed-cooldown-test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const auth = await import("../../src/sse/services/auth.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function seedConnection(provider: string): Promise { + const conn = await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `${provider}-key`, + isActive: true, + testStatus: "active", + }); + return (conn as Record).id as string; +} + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#10347: markAccountUnavailable triggers cooldown on embedding 402", async () => { + await resetStorage(); + const connId = await seedConnection("mistral"); + + const result = await auth.markAccountUnavailable( + connId, + 402, + "Check your subscription on https://admin.mistral.ai/subscription", + "mistral", + "mistral-embed" + ); + + assert.strictEqual(result.shouldFallback, true, "402 must trigger account cooldown"); + + // Verify the connection was marked — 402 is terminal (credits_exhausted), + // which sets testStatus but not rateLimitedUntil + const conn = await providersDb.getProviderConnectionById(connId); + assert.strictEqual( + conn.testStatus, + "credits_exhausted", + "402 must mark connection credits_exhausted" + ); +}); + +test("#10347: markAccountUnavailable triggers cooldown on embedding 500", async () => { + await resetStorage(); + const connId = await seedConnection("mistral"); + + const result = await auth.markAccountUnavailable( + connId, + 500, + "Internal server error", + "mistral", + "mistral-embed" + ); + + assert.strictEqual(result.shouldFallback, true, "500 must trigger account cooldown"); + const conn = await providersDb.getProviderConnectionById(connId); + assert.strictEqual(conn.testStatus, "unavailable", "500 must mark connection unavailable"); +}); + +test("#10347: embedding 400 (bad request) does NOT trigger account cooldown", async () => { + await resetStorage(); + const connId = await seedConnection("mistral"); + + // 400 bad request is a client error, not an account issue — should not cool down + const result = await auth.markAccountUnavailable( + connId, + 400, + "Invalid embedding input format", + "mistral", + "mistral-embed" + ); + + // Generic 400 returns shouldFallback:false (not account-fallback-worthy) + assert.strictEqual(result.shouldFallback, false, "400 bad request must not trigger cooldown"); + const conn = await providersDb.getProviderConnectionById(connId); + assert.strictEqual(conn.testStatus, "active", "400 must keep connection active"); +}); diff --git a/tests/unit/embedding-cooldown-integration-10347.test.ts b/tests/unit/embedding-cooldown-integration-10347.test.ts new file mode 100644 index 0000000000..f7bff8ea7a --- /dev/null +++ b/tests/unit/embedding-cooldown-integration-10347.test.ts @@ -0,0 +1,184 @@ +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"; + +// #10347 integration: exercise createEmbeddingResponse end-to-end with a +// mocked upstream that returns 402, then verify the connection gets cooled +// down. This proves the production code path actually calls +// markAccountUnavailable — the direct-call tests in +// embedding-account-cooldown-10347.test.ts would pass even if the +// production block were removed. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-embed-int-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "embed-int-test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const auth = await import("../../src/sse/services/auth.ts"); + +function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function seedConnection( + provider: string, + overrides: Record = {} +): Promise { + const conn = await providersDb.createProviderConnection({ + provider, + authType: "apikey", + apiKey: `${provider}-key`, + isActive: true, + testStatus: "active", + ...overrides, + }); + return (conn as Record).id as string; +} + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("createEmbeddingResponse marks connection on upstream 402", async () => { + resetStorage(); + const connId = await seedConnection("mistral"); + + // Mock upstream to return 402 (subscription expired). + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + new Response( + JSON.stringify({ error: "Check your subscription on https://admin.mistral.ai/subscription" }), + { status: 402, headers: { "Content-Type": "application/json" } } + )) as typeof globalThis.fetch; + + try { + // Import the service AFTER seeding the DB so its module-level caches + // see the seeded connection. + const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts"); + + // Call the production path — this must exercise the markAccountUnavailable + // block we added in #10347. + const res = await createEmbeddingResponse( + { model: "mistral-embed", input: "hello" }, + { connectionId: connId } + ); + + assert.equal(res.status, 402, "must return upstream status"); + + // Give the fire-and-forget markAccountUnavailable call time to settle. + await new Promise((r) => setImmediate(r)); + await new Promise((r) => setImmediate(r)); + + // Verify the connection was actually marked — this is the assertion + // that would FAIL if the production block were removed. + const conn = await providersDb.getProviderConnectionById(connId); + assert.equal( + conn.testStatus, + "credits_exhausted", + "402 must mark connection credits_exhausted via production code path" + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("cooled account is skipped on next request — second connection selected", async () => { + resetStorage(); + const conn1 = await seedConnection("mistral", { apiKey: "mistral-key-1" }); + const conn2 = await seedConnection("mistral", { apiKey: "mistral-key-2" }); + + const originalFetch = globalThis.fetch; + let fetchCallCount = 0; + + try { + const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts"); + + // First request: upstream returns 402 → conn1 gets cooled. + globalThis.fetch = (async () => { + fetchCallCount++; + return new Response(JSON.stringify({ error: "subscription expired" }), { + status: 402, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof globalThis.fetch; + + const res1 = await createEmbeddingResponse( + { model: "mistral-embed", input: "hello" }, + { connectionId: conn1 } + ); + assert.equal(res1.status, 402); + + // Wait for fire-and-forget cooldown write. + await new Promise((r) => setImmediate(r)); + await new Promise((r) => setImmediate(r)); + + // Verify conn1 is cooled. + const conn1After = await providersDb.getProviderConnectionById(conn1); + assert.equal(conn1After.testStatus, "credits_exhausted", "conn1 must be cooled"); + + // Second request: upstream returns 200. + globalThis.fetch = (async () => { + fetchCallCount++; + return new Response( + JSON.stringify({ + data: [{ embedding: [0.1, 0.2], index: 0 }], + model: "mistral-embed", + usage: { prompt_tokens: 1, total_tokens: 1 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + }) as typeof globalThis.fetch; + + // Call without specifying connectionId — credential selection should + // skip conn1 (credits_exhausted) and pick conn2. + const res2 = await createEmbeddingResponse({ model: "mistral-embed", input: "world" }, {}); + assert.equal(res2.status, 200, "second request must succeed via conn2"); + + // Verify conn2 is still healthy. + const conn2After = await providersDb.getProviderConnectionById(conn2); + assert.equal(conn2After.testStatus, "active", "conn2 must remain active"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("createEmbeddingResponse skips 400 (bad request) — no cooldown", async () => { + resetStorage(); + const connId = await seedConnection("mistral"); + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + new Response(JSON.stringify({ error: "Invalid embedding input format" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + })) as typeof globalThis.fetch; + + try { + const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts"); + + const res = await createEmbeddingResponse( + { model: "mistral-embed", input: "hello" }, + { connectionId: connId } + ); + + assert.equal(res.status, 400, "must return upstream status"); + + await new Promise((r) => setImmediate(r)); + await new Promise((r) => setImmediate(r)); + + const conn = await providersDb.getProviderConnectionById(connId); + assert.equal( + conn.testStatus, + "active", + "400 must NOT mark connection — account is fine, request was wrong" + ); + } finally { + globalThis.fetch = originalFetch; + } +}); From ff9a4c2fbd038bf442af9a9cfd96d707cb3f2db0 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Fri, 21 Aug 2026 00:22:23 +0800 Subject: [PATCH 086/135] fix(combo): prevent unhandledRejection from per-model-timeout abort (#10846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (11/11 focused tests across both new test files, typecheck:core clean after a 1-char fix pushed to this branch: ComboLogger.error is optional in combo/types.ts so the defensive race-catch needed log.error?.(...) — TS2722 otherwise). Solid production diagnosis (47 unhandledRejections traced to the orphaned race loser). Thanks! --- .../services/combo/targetTimeoutRunner.ts | 95 +++++++++++++- open-sse/services/rateLimitManager.ts | 21 ++- .../unit/combo-target-timeout-runner.test.ts | 121 +++++++++++++++++- .../rateLimitManager-queue-timeout.test.ts | 49 +++++++ 4 files changed, 272 insertions(+), 14 deletions(-) diff --git a/open-sse/services/combo/targetTimeoutRunner.ts b/open-sse/services/combo/targetTimeoutRunner.ts index 09a80559b2..4eb6cb8b68 100644 --- a/open-sse/services/combo/targetTimeoutRunner.ts +++ b/open-sse/services/combo/targetTimeoutRunner.ts @@ -19,6 +19,76 @@ import type { HandleSingleModel, SingleModelTarget, ComboLogger } from "./types. /** Stable internal classification for OmniRoute's own combo per-target timer. */ export const COMBO_TARGET_TIMEOUT_CODE = "combo_target_timeout"; +/** + * Diagnostic: track recent combo-per-model-timeout abort errors so an + * unhandledRejection handler can attribute the stack trace to a specific model + * and timeout value. Ring buffer of 4 — concurrent per-model timeouts are rare + * but possible (e.g. hedge + per-target timeout on different targets). + */ +const CONTEXT_RING_SIZE = 4; +const lastTimeoutContexts: Array<{ + modelStr: string; + timeoutMs: number; + abortError: Error; + timestamp: number; +}> = []; +let contextRingIndex = 0; + +function recordTimeoutContext(ctx: { + modelStr: string; + timeoutMs: number; + abortError: Error; + timestamp: number; +}): void { + if (lastTimeoutContexts.length < CONTEXT_RING_SIZE) { + lastTimeoutContexts.push(ctx); + } else { + lastTimeoutContexts[contextRingIndex] = ctx; + contextRingIndex = (contextRingIndex + 1) % CONTEXT_RING_SIZE; + } +} + +/** Retrieve (and clear) all pending combo-per-model-timeout diagnostic contexts. */ +export function drainLastTimeoutContexts(): typeof lastTimeoutContexts { + const out = lastTimeoutContexts.splice(0); + contextRingIndex = 0; + return out; +} + +/** + * Install a persistent unhandledRejection listener that logs combo-per-model-timeout + * diagnostics. Call once at module load. The listener stays installed permanently — + * it only acts on combo-per-model-timeout rejections and returns early for everything + * else, so there is no handler leak and no remove/re-install race window. + */ +let diagnosticInstalled = false; +function ensureDiagnosticListener(): void { + if (diagnosticInstalled) return; + diagnosticInstalled = true; + process.on("unhandledRejection", (reason: unknown) => { + try { + const isComboTimeout = + reason instanceof Error && reason.message === COMBO_PER_MODEL_TIMEOUT_REASON; + if (!isComboTimeout) return; + const contexts = drainLastTimeoutContexts(); + // Log the full stack trace so the next production incident is diagnosable. + // Without this, Node's default unhandledRejection warning shows only + // "Error: combo-per-model-timeout" with no caller context. + const summary = + contexts.length > 0 + ? contexts.map((c) => ` model=${c.modelStr} timeout=${c.timeoutMs}ms`).join("\n") + : " (no context recorded)"; + console.error( + "[COMBO-TIMEOUT-DIAGNOSTIC] unhandledRejection from combo per-model timeout.\n" + + `${summary}\n` + + ` abortError stack:\n${reason.stack ?? reason}` + ); + } catch { + // Diagnostic logging failed — never let this break the process. + } + }); +} + export function buildTargetTimeoutRunner(deps: { handleSingleModel: HandleSingleModel; comboTargetTimeoutMs: number; @@ -29,6 +99,7 @@ export function buildTargetTimeoutRunner(deps: { target?: SingleModelTarget ) => Promise { const { handleSingleModel, comboTargetTimeoutMs, log } = deps; + ensureDiagnosticListener(); return async ( b: Record, modelStr: string, @@ -46,11 +117,18 @@ export function buildTargetTimeoutRunner(deps: { const timeoutPromise = new Promise((resolve) => { timeoutId = setTimeout(() => { timedOut = true; + const abortErr = new Error(COMBO_PER_MODEL_TIMEOUT_REASON); + recordTimeoutContext({ + modelStr, + timeoutMs: comboTargetTimeoutMs, + abortError: abortErr, + timestamp: Date.now(), + }); log.warn( "COMBO", `Model ${modelStr} exceeded ${comboTargetTimeoutMs}ms timeout — falling back` ); - timeoutController.abort(new Error(COMBO_PER_MODEL_TIMEOUT_REASON)); + timeoutController.abort(abortErr); // HTTP 504 (not proprietary 524): this is OmniRoute's own per-target timer. // Typed as combo_target_timeout so request-scoped classification can keep the // connection eligible for fallback instead of treating it like Cloudflare 524 @@ -88,6 +166,13 @@ export function buildTargetTimeoutRunner(deps: { } } try { + // Both branches of the race resolve (never reject): the inner + // handleSingleModel call has a .catch() that converts rejections into + // responses, and timeoutPromise always resolves. A defensive outer + // .catch() guards against unexpected throws in the .catch() handler + // itself (e.g. a broken Error.prototype.message getter) — without + // this, such a throw would surface as an unhandledRejection tagged + // "combo-per-model-timeout" in production logs. return await Promise.race([ handleSingleModel(b, modelStr, targetWithSignal).catch((err) => { if (timedOut) { @@ -99,7 +184,13 @@ export function buildTargetTimeoutRunner(deps: { return errorResponse(502, err?.message ?? "Upstream model error"); }), timeoutPromise, - ]); + ]).catch((raceErr) => { + // Defensive: should never fire — both race branches always resolve. + // Include the error message so the root cause is not masked. + const detail = raceErr instanceof Error ? raceErr.message : String(raceErr); + log.error?.("COMBO", `Unexpected rejection in combo timeout race for ${modelStr}: ${detail}`); + return errorResponse(502, `Combo timeout dispatch error: ${detail}`); + }); } finally { clearTimeout(timeoutId); if (parentHedgeSignal && onParentHedgeAbort) { diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index 3582eec9c0..a7bdeb2b09 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -9,10 +9,7 @@ */ import Bottleneck from "bottleneck"; -import { - applyBottleneckDoExpirePatch, - applyBottleneckHeartbeatPatch, -} from "./bottleneckPatch.ts"; +import { applyBottleneckDoExpirePatch, applyBottleneckHeartbeatPatch } from "./bottleneckPatch.ts"; import { parseRetryAfterFromBody } from "./accountFallback.ts"; import { getAntigravityQuotaFamily } from "./antigravityQuotaFamily.ts"; import { getProviderCategory } from "../config/providerRegistry.ts"; @@ -550,12 +547,7 @@ export async function withRateLimit(provider, connectionId, model, fn, signal = // Proactive sliding-window fallback for header-less providers with a declared cap // (Fase 8.2). No-op unless PROVIDER_DEFAULT_RATE_LIMITS has an entry for `provider`. const maxWaitMs = resolveRequestQueueMaxWaitMs(provider); - await awaitProviderDefaultSlot( - provider, - connectionId, - signal, - maxWaitMs - ); + await awaitProviderDefaultSlot(provider, connectionId, signal, maxWaitMs); const limiter = getLimiter(provider, connectionId, model); // Bottleneck's `expiration` starts only after a job leaves QUEUED. The @@ -607,7 +599,14 @@ export async function withRateLimit(provider, connectionId, model, fn, signal = } try { - return await Promise.race([limiter.schedule(scheduleOpts, fn), abortPromise]); + // Race the work against the abort signal. When abort wins, fn is still + // running inside Bottleneck's limiter — its eventual rejection must not + // surface as an unhandledRejection. The .catch(noop) silences only the + // orphaned branch; the real rejection comes from abortPromise. + const scheduled = limiter.schedule(scheduleOpts, fn); + scheduled.catch(() => {}); // prevent unhandledRejection when abort wins + abortPromise.catch(() => {}); // prevent unhandledRejection when scheduled wins + return await Promise.race([scheduled, abortPromise]); } finally { if (abortListener) { signal.removeEventListener("abort", abortListener); diff --git a/tests/unit/combo-target-timeout-runner.test.ts b/tests/unit/combo-target-timeout-runner.test.ts index e75fee746b..a058a94552 100644 --- a/tests/unit/combo-target-timeout-runner.test.ts +++ b/tests/unit/combo-target-timeout-runner.test.ts @@ -1,6 +1,9 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { buildTargetTimeoutRunner } from "../../open-sse/services/combo/targetTimeoutRunner.ts"; +import { + buildTargetTimeoutRunner, + drainLastTimeoutContexts, +} from "../../open-sse/services/combo/targetTimeoutRunner.ts"; import type { ComboLogger, SingleModelTarget } from "../../open-sse/services/combo/types.ts"; const noopLog: ComboLogger = { warn() {}, info() {}, error() {}, debug() {} }; @@ -85,3 +88,119 @@ test("hedge do parent já abortado propaga o abort ao filho", async () => { await runner({}, "m", parentTarget); assert.equal(sawAbort, true); }); + +test("rejection from handleSingleModel after timeout does not leak as unhandledRejection", async () => { + // Simulate: timeout fires, handleSingleModel later rejects with the abort error. + // Before the fix, this rejection could escape as an unhandledRejection if the + // .catch() handler itself threw or if the promise chain had a gap. + let unhandledRejectionFired = false; + const handler = (reason: unknown) => { + if (reason instanceof Error && reason.message === "combo-per-model-timeout") { + unhandledRejectionFired = true; + } + }; + process.on("unhandledRejection", handler); + + const runner = buildTargetTimeoutRunner({ + handleSingleModel: (_b, _m, target) => + new Promise((_resolve, reject) => { + const sig = target?.modelAbortSignal; + sig?.addEventListener("abort", () => { + // Simulate an upstream that rejects on abort (common pattern). + reject(new Error(sig.reason?.message ?? "aborted")); + }); + }), + comboTargetTimeoutMs: 10, + log: noopLog, + }); + + const res = await runner({}, "test-model"); + assert.equal(res.status, 504, "timeout must win the race"); + + // Drain microtasks — the rejected promise from handleSingleModel should be + // caught by the .catch() handler, not surface as unhandledRejection. + await new Promise((r) => setImmediate(r)); + await new Promise((r) => setImmediate(r)); + + process.removeListener("unhandledRejection", handler); + assert.equal( + unhandledRejectionFired, + false, + "handleSingleModel rejection must be caught, not leak as unhandledRejection" + ); +}); + +test("defensive outer .catch() handles unexpected throws in inner .catch()", async () => { + // Edge case: if the inner .catch() handler itself throws (e.g. a broken + // Error.prototype.message getter), the outer defensive .catch() must + // prevent an unhandledRejection. + let unhandledRejectionFired = false; + const handler = (reason: unknown) => { + if (reason instanceof Error && reason.message === "message getter exploded") { + unhandledRejectionFired = true; + } + }; + process.on("unhandledRejection", handler); + + const runner = buildTargetTimeoutRunner({ + handleSingleModel: async () => { + const err = new Error("upstream-fail"); + // Sabotage the message getter to throw in the .catch() handler. + Object.defineProperty(err, "message", { + get() { + throw new Error("message getter exploded"); + }, + }); + throw err; + }, + comboTargetTimeoutMs: 10000, // long enough that timeout doesn't fire + log: noopLog, + }); + + const res = await runner({}, "broken-model"); + // The defensive outer .catch() should return a 502 instead of letting + // the throw escape. + assert.equal(res.status, 502, "defensive catch must return 502"); + assert.match( + await res.text(), + /message getter exploded/, + "error detail must be included in response" + ); + + // Drain microtasks. + await new Promise((r) => setImmediate(r)); + await new Promise((r) => setImmediate(r)); + + process.removeListener("unhandledRejection", handler); + assert.equal( + unhandledRejectionFired, + false, + "defensive catch must prevent unhandledRejection from inner .catch() throw" + ); +}); + +test("drainLastTimeoutContexts returns and clears recorded contexts", async () => { + // Drain any leftover contexts from previous tests. + drainLastTimeoutContexts(); + + const runner = buildTargetTimeoutRunner({ + handleSingleModel: () => new Promise(() => {}), // never resolves + comboTargetTimeoutMs: 10, + log: noopLog, + }); + + // Fire two timeouts to verify the ring buffer. + await runner({}, "model-a"); + await runner({}, "model-b"); + + const contexts = drainLastTimeoutContexts(); + assert.ok(contexts.length >= 1, "at least one context must be recorded"); + assert.equal(contexts[contexts.length - 1].modelStr, "model-b"); + assert.equal(contexts[contexts.length - 1].timeoutMs, 10); + assert.ok(contexts[contexts.length - 1].abortError instanceof Error); + assert.ok(contexts[contexts.length - 1].timestamp > 0); + + // drain clears the buffer. + const second = drainLastTimeoutContexts(); + assert.equal(second.length, 0, "second drain must return empty"); +}); diff --git a/tests/unit/rateLimitManager-queue-timeout.test.ts b/tests/unit/rateLimitManager-queue-timeout.test.ts index be0a124941..aa57fc3426 100644 --- a/tests/unit/rateLimitManager-queue-timeout.test.ts +++ b/tests/unit/rateLimitManager-queue-timeout.test.ts @@ -36,3 +36,52 @@ test("multiple sequential withRateLimit calls work", async () => { ]); assert.deepEqual(results.sort(), ["a", "b"]); }); + +test("abort signal rejection does not leak as unhandledRejection", async () => { + // Simulate the combo-per-model-timeout scenario: abort signal fires while + // fn is running inside Bottleneck's limiter. The abortPromise rejects and + // wins Promise.race, but fn's eventual rejection must be silently caught + // (not surface as unhandledRejection). + enableRateLimitProtection("test-queue-abort"); + + let unhandledRejectionFired = false; + const handler = (reason: unknown) => { + if (reason instanceof Error && reason.message === "combo-per-model-timeout") { + unhandledRejectionFired = true; + } + }; + process.on("unhandledRejection", handler); + + const ac = new AbortController(); + const err = new Error("combo-per-model-timeout"); + + // Schedule a slow function, then abort mid-flight. + const promise = withRateLimit( + "openai", + "test-queue-abort", + "gpt-4", + async () => { + // Simulate work that respects the abort signal (like a fetch). + await new Promise((r) => setTimeout(r, 200)); + throw err; + }, + ac.signal + ); + + // Abort quickly so abortPromise wins the race. + setTimeout(() => ac.abort(err), 10); + + // The withRateLimit call itself should reject (from abortPromise). + await assert.rejects(promise, (e: Error) => e.message === "combo-per-model-timeout"); + + // Give Bottleneck time to finish the orphaned job and let any + // unhandledRejection fire. + await new Promise((r) => setTimeout(r, 500)); + + process.removeListener("unhandledRejection", handler); + assert.equal( + unhandledRejectionFired, + false, + "fn rejection after abort must be silently caught, not leak as unhandledRejection" + ); +}); From 621f30a1883e986e09e7ff44bc6f07442a24e294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=A6=8D=E5=84=BF=20=E2=9C=A8?= Date: Fri, 21 Aug 2026 02:07:19 +0800 Subject: [PATCH 087/135] fix(cli): restore packaged machine-token authentication (#10468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado por restaurar e endurecer a autenticação por machine-token no CLI empacotado. Validação (worktree combinado a partir de origin/release/v3.8.50, merge limpo, 0 conflitos — 34 arquivos, +1078/-247): - `npm run typecheck:core` — limpo - `node scripts/check/check-complexity.mjs` — OK (2558 violações vs baseline 2774) - `node scripts/check/check-cognitive-complexity.mjs` — OK (1152 violações vs baseline 1223) - `node scripts/check/check-file-size.mjs` — OK - `node scripts/check/check-changelog-integrity.mjs` — OK - Testes focados (8 arquivos: cli-doctor-command, cli-machine-token, lib/machineToken, lib/managementCliToken, agentSkills-generator, api/settings-audit, check-pack-boot, next-config) — 95/95 passando Os dois achados de segurança do maintainer-feedback original (checagem de loopback tipo SSRF, escopo de cookie/CSRF) já estavam corrigidos e cobertos por teste no commit `2b785f0068a862fbd867221294325ad921787782` desta branch. --- bin/cli/api.mjs | 36 ++++- bin/cli/commands/doctor.mjs | 99 ++++++++++++- bin/cli/utils/cliToken.mjs | 34 +++-- docs/security/CLI_TOKEN.md | 23 ++-- scripts/build/postinstall.mjs | 25 ++++ scripts/check/check-pack-boot.mjs | 134 ++++++++++++++++-- skills/omni-api-keys/SKILL.md | 4 +- skills/omni-auth/SKILL.md | 14 +- skills/omni-budget/SKILL.md | 2 +- skills/omni-cli-tools/SKILL.md | 26 ++-- skills/omni-combos-routing/SKILL.md | 8 +- skills/omni-compression/SKILL.md | 2 +- skills/omni-context-rtk/SKILL.md | 6 +- skills/omni-inference/SKILL.md | 38 ++--- skills/omni-models/SKILL.md | 2 +- skills/omni-providers/SKILL.md | 24 ++-- skills/omni-settings/SKILL.md | 34 ++--- skills/omni-sync-cloud/SKILL.md | 12 +- skills/omni-usage-logs/SKILL.md | 2 +- skills/omni-version-manager/SKILL.md | 78 +++++------ src/app/api/settings/route.ts | 3 + src/lib/agentSkills/generator.ts | 55 ++++++-- src/lib/api/requireManagementAuth.ts | 13 +- src/lib/machineToken.ts | 27 ++-- src/server/authz/pipeline.ts | 6 + src/server/authz/policies/management.ts | 1 + tests/unit/agentSkills-generator.test.ts | 135 +++++++++++------- tests/unit/api/settings-audit.test.ts | 22 +++ tests/unit/check-pack-boot.test.ts | 72 ++++++++++ tests/unit/cli-doctor-command.test.ts | 161 ++++++++++++++++++++++ tests/unit/cli-machine-token.test.ts | 150 ++++++++++++++++++++ tests/unit/lib/machineToken.test.ts | 28 +++- tests/unit/lib/managementCliToken.test.ts | 37 ++++- tests/unit/next-config.test.ts | 12 +- 34 files changed, 1078 insertions(+), 247 deletions(-) diff --git a/bin/cli/api.mjs b/bin/cli/api.mjs index fff6cf0829..6534f91095 100644 --- a/bin/cli/api.mjs +++ b/bin/cli/api.mjs @@ -52,6 +52,19 @@ function resolveUrl(path, opts) { return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`; } +/** The machine-derived token is valid only for the local loopback server. */ +export function isLoopbackUrl(value) { + try { + const hostname = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase(); + if (hostname === "localhost" || hostname === "::1") return true; + if (/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)) return true; + if (/^::ffff:(?:127\.|7f[0-9a-f]{2}:)/i.test(hostname)) return true; + return false; + } catch { + return false; + } +} + export async function buildHeaders(opts) { const headers = new Headers(opts.headers || {}); if (!headers.has("accept")) headers.set("accept", "application/json"); @@ -87,10 +100,17 @@ export async function buildHeaders(opts) { if (auth && !headers.has("authorization")) { headers.set("authorization", `Bearer ${auth}`); } - // Inject machine-id derived CLI token; env var override for testing. - const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken()); - if (cliToken && !headers.has(CLI_TOKEN_HEADER)) { - headers.set(CLI_TOKEN_HEADER, cliToken); + // Inject the machine-derived credential only for an explicit local loopback + // destination. Remote contexts and absolute remote URLs use scoped access + // tokens and must never receive this machine-bound local credential. + const destinationUrl = opts.destinationUrl ?? getBaseUrl(opts); + if (!isLoopbackUrl(destinationUrl)) { + headers.delete(CLI_TOKEN_HEADER); + } else { + const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken()); + if (cliToken && !headers.has(CLI_TOKEN_HEADER)) { + headers.set(CLI_TOKEN_HEADER, cliToken); + } } if (opts.idempotencyKey && !headers.has("idempotency-key")) { headers.set("idempotency-key", opts.idempotencyKey); @@ -195,8 +215,12 @@ function fetchOnce(url, init, timeoutMs) { export async function apiFetch(path, opts = {}) { const method = String(opts.method || "GET").toUpperCase(); const url = resolveUrl(path, opts); - const headers = await buildHeaders(opts); + const headers = await buildHeaders({ ...opts, destinationUrl: url }); const body = serializeBody(opts.body, headers); + // Undici preserves custom headers across cross-origin redirects. A local server + // redirect must never turn the loopback machine credential into an outbound + // secret, so fail redirects whenever this header is present. + const redirect = headers.has(CLI_TOKEN_HEADER) ? "error" : opts.redirect; const timeout = opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000); const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts); @@ -205,7 +229,7 @@ export async function apiFetch(path, opts = {}) { let lastErr; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - const res = await fetchOnce(url, { method, headers, body }, timeout); + const res = await fetchOnce(url, { method, headers, body, redirect }, timeout); if (res.ok) return enrichResponse(res, opts); if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) { const delay = computeBackoff(attempt, res.headers.get("retry-after")); diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs index 9ac34bf636..817013dd19 100644 --- a/bin/cli/commands/doctor.mjs +++ b/bin/cli/commands/doctor.mjs @@ -4,7 +4,9 @@ import os from "node:os"; import path from "node:path"; import { createDecipheriv, scryptSync } from "node:crypto"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { isLoopbackUrl } from "../api.mjs"; import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs"; +import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs"; import { printHeading } from "../io.mjs"; import { t } from "../i18n.mjs"; import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs"; @@ -378,11 +380,11 @@ function checkMemory() { }); } -async function fetchWithTimeout(url) { +async function fetchWithTimeout(url, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS); try { - return await fetch(url, { signal: controller.signal }); + return await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(timeout); } @@ -471,6 +473,98 @@ async function checkServerLiveness(options = {}) { ); } +export async function checkMachineTokenAuth(options = {}) { + if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") { + return warn("CLI machine token", "CLI machine-token authentication is disabled", { + derived: false, + accepted: false, + disabled: true, + tokenExposed: false, + }); + } + + let url; + try { + const parsed = new URL(resolveLivenessUrl(options)); + if ( + !["http:", "https:"].includes(parsed.protocol) || + parsed.username || + parsed.password || + !isLoopbackUrl(parsed.toString()) + ) { + return warn( + "CLI machine token", + "Machine-token probes are limited to HTTP(S) loopback endpoints", + { derived: false, accepted: false, tokenExposed: false } + ); + } + parsed.pathname = "/api/cli/whoami"; + parsed.search = ""; + parsed.hash = ""; + url = parsed.toString(); + } catch { + return warn("CLI machine token", "Could not resolve the management endpoint", { + derived: false, + accepted: false, + tokenExposed: false, + }); + } + + const token = await getCliToken(); + if (!token) { + return fail( + "CLI machine token", + "Could not derive a machine token; verify the node-machine-id runtime is installed", + { derived: false, accepted: false, tokenExposed: false } + ); + } + + try { + const response = await fetchWithTimeout(url, { + headers: { [CLI_TOKEN_HEADER]: token }, + redirect: "error", + }); + if (response.ok) { + return ok("CLI machine token", "Server accepted the local machine token", { + url, + status: response.status, + derived: true, + accepted: true, + tokenExposed: false, + }); + } + if (response.status === 401 || response.status === 403) { + return warn( + "CLI machine token", + "Server rejected the local machine token; if the CLI and server are on different hosts or container boundaries, run `omniroute connect --key `", + { + url, + status: response.status, + derived: true, + accepted: false, + containerBoundaryLikely: true, + tokenExposed: false, + } + ); + } + return warn("CLI machine token", `Machine-token probe returned HTTP ${response.status}`, { + url, + status: response.status, + derived: true, + accepted: false, + tokenExposed: false, + }); + } catch { + return warn("CLI machine token", "Machine-token endpoint could not be reached", { + url, + status: 0, + derived: true, + accepted: false, + tokenExposed: false, + }); + } +} + export async function collectDoctorChecks(context = {}, options = {}) { const rootDir = context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); @@ -488,6 +582,7 @@ export async function collectDoctorChecks(context = {}, options = {}) { if (!options.skipLiveness) { checks.push(await checkServerLiveness(options)); + checks.push(await checkMachineTokenAuth(options)); } // CLI tool health checks diff --git a/bin/cli/utils/cliToken.mjs b/bin/cli/utils/cliToken.mjs index 94691ba952..38895c13bf 100644 --- a/bin/cli/utils/cliToken.mjs +++ b/bin/cli/utils/cliToken.mjs @@ -12,25 +12,39 @@ function getActiveSalt() { return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT; } -export async function getCliToken() { - const salt = getActiveSalt(); - if (_cached !== null && _cachedSalt === salt) return _cached; +export function deriveCliToken(machineIdModule, salt) { try { // node-machine-id is CommonJS: under `await import()` its exports land on // `.default`, so destructuring `machineIdSync` off the namespace yields // undefined and calling it throws — which the catch below turned into an // empty token, silently disabling CLI auth for every management request. // Same resolution order as src/lib/machineToken.ts. - const mod = await import("node-machine-id"); - const machineIdSync = mod.machineIdSync ?? mod.default?.machineIdSync; - if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable"); + const machineIdSync = + machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync; + if (typeof machineIdSync !== "function") return ""; // machineIdSync(true) returns the original unhashed hardware ID — mirrors // getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening). - const mid = machineIdSync(true); - _cached = crypto.createHmac("sha256", mid).update(salt).digest("hex"); + const rawId = machineIdSync(true); + if (!rawId) return ""; + return crypto.createHmac("sha256", rawId).update(salt).digest("hex"); + } catch { + return ""; + } +} + +export async function getCliToken() { + const salt = getActiveSalt(); + if (_cached !== null && _cachedSalt === salt) return _cached; + try { + const imported = await import("node-machine-id"); + const token = deriveCliToken(imported, salt); + if (!token) { + // Swallowing here changes control flow (every management call goes out + // unauthenticated and 401s), so leave a breadcrumb rather than failing mute. + console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled"); + } + _cached = token; } catch (e) { - // Swallowing here changes control flow (every management call goes out - // unauthenticated and 401s), so leave a breadcrumb rather than failing mute. console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled:", e); _cached = ""; } diff --git a/docs/security/CLI_TOKEN.md b/docs/security/CLI_TOKEN.md index 1e00e22334..4d3383229d 100644 --- a/docs/security/CLI_TOKEN.md +++ b/docs/security/CLI_TOKEN.md @@ -20,21 +20,26 @@ password on every invocation. (falls back to an empty string on failure, disabling CLI auth). 2. It computes `HMAC-SHA256(machine_id, salt)` and returns the full 64-char hex digest — a deterministic, non-reversible token tied to this machine. -3. The CLI sends the token as `x-omniroute-cli-token` on every request to - `http://localhost:/api/...`. +3. The CLI sends the token as `x-omniroute-cli-token` only when the resolved + destination is an explicit loopback URL (`localhost`, `127.0.0.0/8`, or + loopback IPv6). Requests carrying the token use `redirect: error`, so a local + redirect cannot forward it to another origin. Remote contexts use scoped + access tokens instead. If derivation is unavailable, the CLI omits the header + and `omniroute doctor` reports the failure instead of treating an empty token + as valid. 4. The server (`src/server/authz/policies/management.ts`) recomputes the expected token with the same salt and compares via `timingSafeEqual` to prevent timing-based extraction. ## Security properties -| Property | Detail | -| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| **Loopback-only** | Accepted only when `Host` is `localhost`, `127.0.0.1`, or `::1`. | -| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. | -| **Non-reversible** | HMAC output cannot recover the machine-id. | -| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. | -| **Non-exportable** | Token is never written to disk or logged. | +| Property | Detail | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Loopback-only** | Accepted only when the server's trusted peer-locality stamp (derived from the real TCP peer address) says loopback. The client-controlled `Host` header is never trusted for locality. | +| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. | +| **Non-reversible** | HMAC output cannot recover the machine-id. | +| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. | +| **Non-exportable** | Token is never written to disk or logged. | ## Salt rotation diff --git a/scripts/build/postinstall.mjs b/scripts/build/postinstall.mjs index 9570691b46..1628aca7cf 100644 --- a/scripts/build/postinstall.mjs +++ b/scripts/build/postinstall.mjs @@ -16,6 +16,8 @@ * - better-sqlite3 (SQLite bindings) * - wreq-js (TLS client for OAuth providers) * - tls-client-node (TLS client for chatgpt-web/claude-web/grok-web/lmarena/perplexity-web) + * - sql.js (WASM SQLite fallback runtime) + * - node-machine-id (local CLI machine-token server runtime) * * Fixes: https://github.com/diegosouzapw/OmniRoute/issues/129 * Fixes: https://github.com/diegosouzapw/OmniRoute/issues/321 @@ -33,6 +35,7 @@ import { readdirSync, writeFileSync, } from "node:fs"; +import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -45,6 +48,7 @@ import { fixPlaywrightAndroid } from "./fixPlaywrightAndroid.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, "..", ".."); +const requireFromPackage = createRequire(join(ROOT, "package.json")); /** * Patch node-gyp's common.gypi to include the android_ndk_path variable. @@ -437,12 +441,33 @@ async function verifyDevNativeModules() { } } +async function ensureStandaloneRuntimePackages() { + for (const packageName of ["sql.js", "node-machine-id"]) { + let source; + try { + source = dirname(dirname(requireFromPackage.resolve(packageName))); + } catch { + console.warn(` ⚠️ ${packageName} could not be resolved from the npm install.`); + continue; + } + const destination = join(ROOT, "dist", "node_modules", packageName); + try { + mkdirSync(dirname(destination), { recursive: true }); + cpSync(source, destination, { recursive: true, force: true }); + console.log(` ✅ ${packageName} copied to standalone dist/node_modules.`); + } catch (err) { + console.warn(` ⚠️ Could not copy ${packageName}: ${err.message}`); + } + } +} + await verifyDevNativeModules(); await fixBetterSqliteBinary(); await fixWreqJsBinary(); await fixTlsClientNodeBinary({ rootDir: ROOT }); await fixPlaywrightAndroid({ rootDir: ROOT }); await ensureSwcHelpers(); +await ensureStandaloneRuntimePackages(); await ensureLlmlinguaOptionals(); await syncProjectEnv(); diff --git a/scripts/check/check-pack-boot.mjs b/scripts/check/check-pack-boot.mjs index 9eabab477a..673decdd21 100644 --- a/scripts/check/check-pack-boot.mjs +++ b/scripts/check/check-pack-boot.mjs @@ -14,13 +14,17 @@ * 0 = boots and reports the right version · 1 = boot failed · 2 = missing build. */ import { execFileSync, spawn } from "node:child_process"; +import { createHmac } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { pathToFileURL } from "node:url"; const POLL_INTERVAL_MS = 2_000; const BOOT_DEADLINE_MS = 240_000; +const MAX_SERVER_OUTPUT_CHARS = 1_000_000; const SQLJS_STARTUP_MARKER = "Pre-initializing sql.js WASM"; +const DEFAULT_CLI_SALT = "omniroute-cli-auth-v1"; export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([ "dist/node_modules/sql.js/package.json", @@ -28,6 +32,11 @@ export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([ "dist/node_modules/sql.js/dist/sql-wasm.wasm", ]); +export const REQUIRED_MACHINE_TOKEN_RUNTIME_FILES = Object.freeze([ + "node_modules/node-machine-id/package.json", + "node_modules/node-machine-id/index.js", +]); + /** Parse `npm pack --json` output into the generated tarball filename. */ export function pickTarball(packJsonOutput) { const parsed = JSON.parse(packJsonOutput); @@ -62,6 +71,39 @@ export function findMissingSqlJsRuntimeFiles(packageRoot, exists = fs.existsSync ); } +export function findMissingMachineTokenRuntimeFiles(packageRoot, exists = fs.existsSync) { + return REQUIRED_MACHINE_TOKEN_RUNTIME_FILES.filter( + (relativePath) => !exists(path.join(packageRoot, relativePath)) + ); +} + +export function evaluateMachineTokenAuth({ + cliToken, + unauthenticatedStatus, + invalidStatus, + authenticatedStatus, + salt = process.env.OMNIROUTE_CLI_SALT || DEFAULT_CLI_SALT, +}) { + const failures = []; + if (!/^[0-9a-f]{64}$/.test(cliToken || "")) { + failures.push("packaged CLI derived an empty or malformed machine token"); + } + const emptyMachineIdToken = createHmac("sha256", "").update(salt).digest("hex"); + if (cliToken === emptyMachineIdToken) { + failures.push("packaged CLI derived the public empty-machine-id token"); + } + if (unauthenticatedStatus !== 401) { + failures.push(`no-credential request returned ${unauthenticatedStatus} (expected 401)`); + } + if (invalidStatus !== 401) { + failures.push(`invalid-token request returned ${invalidStatus} (expected 401)`); + } + if (authenticatedStatus !== 200) { + failures.push(`packaged CLI token request returned ${authenticatedStatus} (expected 200)`); + } + return { ok: failures.length === 0, failures }; +} + export function evaluateSqlJsRoundTrip({ startupOutput, beforeValue, @@ -106,8 +148,9 @@ async function readJsonResponse(url, options) { return { response, body }; } -async function verifySettingsRoundTrip(baseUrl, startupOutput) { - const initial = await readJsonResponse(`${baseUrl}/api/settings`); +async function verifySettingsRoundTrip(baseUrl, startupOutput, cliToken) { + const authHeaders = { "x-omniroute-cli-token": cliToken }; + const initial = await readJsonResponse(`${baseUrl}/api/settings`, { headers: authHeaders }); if (initial.response.status !== 200 || !initial.body || typeof initial.body !== "object") { return { ok: false, @@ -119,7 +162,7 @@ async function verifySettingsRoundTrip(baseUrl, startupOutput) { const expectedValue = !beforeValue; const patched = await readJsonResponse(`${baseUrl}/api/settings`, { method: "PATCH", - headers: { "Content-Type": "application/json" }, + headers: { ...authHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ debugMode: expectedValue }), }); if (patched.response.status !== 200 || !patched.body || typeof patched.body !== "object") { @@ -129,7 +172,7 @@ async function verifySettingsRoundTrip(baseUrl, startupOutput) { }; } - const readBack = await readJsonResponse(`${baseUrl}/api/settings`); + const readBack = await readJsonResponse(`${baseUrl}/api/settings`, { headers: authHeaders }); if (readBack.response.status !== 200 || !readBack.body || typeof readBack.body !== "object") { return { ok: false, @@ -242,22 +285,61 @@ function spawnServer(binPath, port, dataDir) { OMNIROUTE_SKIP_SYSTEM_TRUST: "1", OMNIROUTE_PACK_BOOT_SMOKE: "1", OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1", + INITIAL_PASSWORD: "pack-boot-machine-token-auth-required", }, stdio: ["ignore", "pipe", "pipe"], detached: true, }); const tail = []; + let retainedChars = 0; const keepTail = (chunk) => { - tail.push(String(chunk)); - while (tail.length > 80) tail.shift(); + const text = String(chunk); + tail.push(text); + retainedChars += text.length; + while (retainedChars > MAX_SERVER_OUTPUT_CHARS && tail.length > 1) { + retainedChars -= tail.shift().length; + } }; child.stdout.on("data", keepTail); child.stderr.on("data", keepTail); return { child, tail }; } +function derivePackagedCliToken(packageRoot) { + const cliModuleUrl = pathToFileURL( + path.join(packageRoot, "bin", "cli", "utils", "cliToken.mjs") + ).href; + return execFileSync( + process.execPath, + [ + "--input-type=module", + "--eval", + "import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))", + cliModuleUrl, + ], + { encoding: "utf8", env: { ...process.env } } + ).trim(); +} + +async function verifyMachineTokenAuth(baseUrl, cliToken) { + const endpoint = `${baseUrl}/api/cli/whoami`; + const unauthenticatedStatus = (await fetch(endpoint)).status; + const invalidStatus = ( + await fetch(endpoint, { headers: { "x-omniroute-cli-token": "0".repeat(64) } }) + ).status; + const authenticatedStatus = ( + await fetch(endpoint, { headers: { "x-omniroute-cli-token": cliToken } }) + ).status; + return evaluateMachineTokenAuth({ + cliToken, + unauthenticatedStatus, + invalidStatus, + authenticatedStatus, + }); +} + /** Poll /api/monitoring/health until the packed version answers or the boot deadline passes. */ -async function waitForHealthy(port, child, expectedVersion) { +async function waitForHealthy(port, child, expectedVersion, cliToken) { // Seed from authoritative state (Node sets these synchronously at death), then attach a // named once-listener, then re-check: a child that died before this call, or in the gap // before the listener attached, would otherwise never fire "exit" and waste the deadline. @@ -279,7 +361,9 @@ async function waitForHealthy(port, child, expectedVersion) { return { ok: false, failures: [`process exited (${childExit}) before serving`] }; } try { - const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`); + const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, { + headers: { "x-omniroute-cli-token": cliToken }, + }); const body = await res.json().catch(() => null); verdict = evaluateBoot(res.status, body, expectedVersion); if (verdict.ok) return verdict; @@ -299,8 +383,10 @@ async function waitForHealthy(port, child, expectedVersion) { * field throws: coercing with `=== true` would read `false` for a malformed response and * could falsely "pass" persistence whenever the expected value happens to be false. */ -async function readSettingsDebugMode(baseUrl) { - const { response, body } = await readJsonResponse(`${baseUrl}/api/settings`); +async function readSettingsDebugMode(baseUrl, cliToken) { + const { response, body } = await readJsonResponse(`${baseUrl}/api/settings`, { + headers: { "x-omniroute-cli-token": cliToken }, + }); if (response.status !== 200 || !body || typeof body !== "object") { throw new Error(`settings GET HTTP ${response.status} or non-JSON body`); } @@ -350,21 +436,38 @@ async function main() { ); } log("installed package contains the complete sql.js WASM runtime"); + const missingMachineTokenFiles = findMissingMachineTokenRuntimeFiles(packageRoot); + if (missingMachineTokenFiles.length > 0) { + throw new Error( + `installed package is missing the node-machine-id runtime contract: ${missingMachineTokenFiles.join(", ")}` + ); + } + log("installed package contains the node-machine-id runtime"); const port = pickPort(); const dataDir = path.join(tmp, "data"); fs.mkdirSync(dataDir, { recursive: true }); const binPath = path.join(prefix, "bin", "omniroute"); + const packagedCliToken = derivePackagedCliToken(packageRoot); // BOOT #1 — boot, prove the forced sql.js tier, PATCH a setting, then shut down cleanly // so the sql.js adapter's graceful persist actually lands on disk. The in-flow stopChild // THROWS on failure; that lands in catch as primaryError and boot #2 never starts. log(`boot #1: installed CLI on :${port} (DATA_DIR isolated)…`); ({ child, tail } = spawnServer(binPath, port, dataDir)); - let verdict = await waitForHealthy(port, child, expectedVersion); + let verdict = await waitForHealthy(port, child, expectedVersion, packagedCliToken); if (verdict.ok) { log(`healthy: HTTP 200, version ${expectedVersion}`); - const roundTrip = await verifySettingsRoundTrip(`http://127.0.0.1:${port}`, tail.join("")); + const baseUrl = `http://127.0.0.1:${port}`; + const machineAuth = await verifyMachineTokenAuth(baseUrl, packagedCliToken); + if (!machineAuth.ok) { + verdict = machineAuth; + } else { + log("machine-token auth passed with no/invalid/valid contrast controls"); + } + const roundTrip = verdict.ok + ? await verifySettingsRoundTrip(baseUrl, tail.join(""), packagedCliToken) + : { ok: false, failures: verdict.failures }; if (roundTrip.ok) { log("settings write/read succeeded through the forced sql.js driver"); await stopChild(child); // throws here → primaryError; boot #2 is skipped @@ -373,10 +476,13 @@ async function main() { // BOOT #2 — same DATA_DIR, fresh process: the value must be read back FROM DISK. log("boot #2: rebooting on the same DATA_DIR to prove disk persistence…"); ({ child, tail } = spawnServer(binPath, port, dataDir)); - verdict = await waitForHealthy(port, child, expectedVersion); + verdict = await waitForHealthy(port, child, expectedVersion, packagedCliToken); if (verdict.ok) { log(`healthy: HTTP 200, version ${expectedVersion}`); - const restartValue = await readSettingsDebugMode(`http://127.0.0.1:${port}`); + const restartValue = await readSettingsDebugMode( + `http://127.0.0.1:${port}`, + packagedCliToken + ); const persistence = evaluateRestartPersistence({ expectedValue: roundTrip.expectedValue, restartValue, diff --git a/skills/omni-api-keys/SKILL.md b/skills/omni-api-keys/SKILL.md index 7e6169879d..7501714b01 100644 --- a/skills/omni-api-keys/SKILL.md +++ b/skills/omni-api-keys/SKILL.md @@ -29,7 +29,7 @@ Create API key ```bash curl -X POST https://localhost:20128/api/keys \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -49,7 +49,7 @@ Update API key ```bash curl -X PATCH https://localhost:20128/api/keys/{id} \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-auth/SKILL.md b/skills/omni-auth/SKILL.md index 9fdbebb75a..cd909eb51d 100644 --- a/skills/omni-auth/SKILL.md +++ b/skills/omni-auth/SKILL.md @@ -10,7 +10,7 @@ Manage API key authentication and session tokens. Start here to authenticate req ## Authentication -All requests require a valid Bearer token or session cookie. Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development. +Remote API requests use a Bearer credential. Dashboard login is different: `POST /api/auth/login` accepts a management password and returns an `auth_token` session cookie. ## Endpoints @@ -20,9 +20,9 @@ Authenticate user ```bash curl -X POST https://localhost:20128/api/auth/login \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" -H "Content-Type: application/json" \ - -d '{}' + -c cookie.jar \ + -d '{"password":""}' ``` ### POST /api/auth/logout @@ -30,8 +30,10 @@ curl -X POST https://localhost:20128/api/auth/login \ Log out ```bash +CSRF_TOKEN=$(curl -s https://localhost:20128/api/auth/csrf -b cookie.jar | jq -r .token) curl -X POST https://localhost:20128/api/auth/logout \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -b cookie.jar \ + -H "x-omniroute-csrf: $CSRF_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -48,7 +50,7 @@ remains available as a fallback while OIDC is enabled. ```bash curl https://localhost:20128/api/auth/oidc/login \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -b cookie.jar ``` ### GET /api/auth/oidc/callback @@ -64,7 +66,7 @@ JWT used by password login and redirects to `/dashboard`. ```bash curl https://localhost:20128/api/auth/oidc/callback \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -b cookie.jar ``` ## Payloads diff --git a/skills/omni-budget/SKILL.md b/skills/omni-budget/SKILL.md index 09744616d2..a7b8fd67d1 100644 --- a/skills/omni-budget/SKILL.md +++ b/skills/omni-budget/SKILL.md @@ -29,7 +29,7 @@ Update rate limit configuration ```bash curl -X POST https://localhost:20128/api/rate-limit \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-cli-tools/SKILL.md b/skills/omni-cli-tools/SKILL.md index 76c3e54b23..b5376cf960 100644 --- a/skills/omni-cli-tools/SKILL.md +++ b/skills/omni-cli-tools/SKILL.md @@ -29,7 +29,7 @@ Create CLI tool backup ```bash curl -X POST https://localhost:20128/api/cli-tools/backups \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -67,7 +67,7 @@ Update Antigravity MITM proxy settings ```bash curl -X POST https://localhost:20128/api/cli-tools/antigravity-mitm \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -96,7 +96,7 @@ Update Antigravity MITM alias configuration ```bash curl -X PUT https://localhost:20128/api/cli-tools/antigravity-mitm/alias \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -116,7 +116,7 @@ Apply Claude CLI settings ```bash curl -X POST https://localhost:20128/api/cli-tools/claude-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -145,7 +145,7 @@ Apply Cline CLI settings ```bash curl -X POST https://localhost:20128/api/cli-tools/cline-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -174,7 +174,7 @@ Create Codex profile ```bash curl -X POST https://localhost:20128/api/cli-tools/codex-profiles \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -185,7 +185,7 @@ Update Codex profile ```bash curl -X PUT https://localhost:20128/api/cli-tools/codex-profiles \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -214,7 +214,7 @@ Apply Codex CLI settings ```bash curl -X POST https://localhost:20128/api/cli-tools/codex-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -243,7 +243,7 @@ Apply Droid CLI settings ```bash curl -X POST https://localhost:20128/api/cli-tools/droid-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -272,7 +272,7 @@ Apply Kilo CLI settings ```bash curl -X POST https://localhost:20128/api/cli-tools/kilo-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -301,7 +301,7 @@ Apply OpenClaw CLI settings ```bash curl -X POST https://localhost:20128/api/cli-tools/openclaw-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -334,7 +334,7 @@ Local-only. Registers OmniRoute as an `openai-compat` provider in Crush's config ```bash curl -X POST https://localhost:20128/api/cli-tools/crush-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -369,7 +369,7 @@ Local-only. Writes the OmniRoute config block in CodeWhale TOML format. ```bash curl -X POST https://localhost:20128/api/cli-tools/codewhale-settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-combos-routing/SKILL.md b/skills/omni-combos-routing/SKILL.md index 6558a3b39c..9745bf9085 100644 --- a/skills/omni-combos-routing/SKILL.md +++ b/skills/omni-combos-routing/SKILL.md @@ -29,7 +29,7 @@ Create routing combo ```bash curl -X POST https://localhost:20128/api/combos \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -40,7 +40,7 @@ Update combo ```bash curl -X PATCH https://localhost:20128/api/combos/{id} \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -69,7 +69,7 @@ Test a combo configuration ```bash curl -X POST https://localhost:20128/api/combos/test \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -93,7 +93,7 @@ Registers a fallback routing chain for a model. ```bash curl -X POST https://localhost:20128/api/fallback/chains \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-compression/SKILL.md b/skills/omni-compression/SKILL.md index eb89f08ccf..f17f39c52f 100644 --- a/skills/omni-compression/SKILL.md +++ b/skills/omni-compression/SKILL.md @@ -20,7 +20,7 @@ Preview compression for a message payload ```bash curl -X POST https://localhost:20128/api/compression/preview \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-context-rtk/SKILL.md b/skills/omni-context-rtk/SKILL.md index 8bad8466e5..8f75be1881 100644 --- a/skills/omni-context-rtk/SKILL.md +++ b/skills/omni-context-rtk/SKILL.md @@ -29,7 +29,7 @@ Update RTK compression settings ```bash curl -X PUT https://localhost:20128/api/context/rtk/config \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -49,7 +49,7 @@ Validate or install an RTK TOML schema v1 filter file ```bash curl -X POST https://localhost:20128/api/context/rtk/import \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -60,7 +60,7 @@ Run RTK compression preview for text ```bash curl -X POST https://localhost:20128/api/context/rtk/test \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-inference/SKILL.md b/skills/omni-inference/SKILL.md index e6a787c259..2951ce794b 100644 --- a/skills/omni-inference/SKILL.md +++ b/skills/omni-inference/SKILL.md @@ -40,7 +40,7 @@ OpenAI-compatible chat completions endpoint. Routes to configured providers. ```bash curl -X POST https://localhost:20128/api/v1/chat/completions \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -64,7 +64,7 @@ Routes to a specific provider by name. ```bash curl -X POST https://localhost:20128/api/v1/providers/{provider}/chat/completions \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -77,7 +77,7 @@ Provides compatibility with Ollama's /api/chat format. ```bash curl -X POST https://localhost:20128/api/v1/api/chat \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -90,7 +90,7 @@ Anthropic Messages API endpoint. Routes to Claude providers. ```bash curl -X POST https://localhost:20128/api/v1/messages \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -101,7 +101,7 @@ Count tokens for a message ```bash curl -X POST https://localhost:20128/api/v1/messages/count_tokens \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -114,7 +114,7 @@ OpenAI Responses API endpoint. ```bash curl -X POST https://localhost:20128/api/v1/responses \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -125,7 +125,7 @@ Create embeddings ```bash curl -X POST https://localhost:20128/api/v1/embeddings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -158,7 +158,7 @@ Create embeddings (provider-specific) ```bash curl -X POST https://localhost:20128/api/v1/providers/{provider}/embeddings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -169,7 +169,7 @@ Generate images ```bash curl -X POST https://localhost:20128/api/v1/images/generations \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -180,7 +180,7 @@ Generate images (provider-specific) ```bash curl -X POST https://localhost:20128/api/v1/providers/{provider}/images/generations \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -193,7 +193,7 @@ Text-to-speech endpoint. Routes to configured TTS providers. ```bash curl -X POST https://localhost:20128/api/v1/audio/speech \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -206,7 +206,7 @@ Audio-to-text transcription endpoint. ```bash curl -X POST https://localhost:20128/api/v1/audio/transcriptions \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -219,7 +219,7 @@ Content moderation endpoint. Routes to configured moderation providers. ```bash curl -X POST https://localhost:20128/api/v1/moderations \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -232,7 +232,7 @@ Document reranking endpoint. ```bash curl -X POST https://localhost:20128/api/v1/rerank \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -278,7 +278,7 @@ Creates a subscription record. If `mode` is `rule`, at least one entry in `ruleP ```bash curl -X POST https://localhost:20128/api/v1/management/proxy-subscriptions \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -300,7 +300,7 @@ Partial update — only fields present in the body are changed (name/url/mode/ru ```bash curl -X PATCH https://localhost:20128/api/v1/management/proxy-subscriptions/{id} \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -335,7 +335,7 @@ Re-fetches and re-parses the subscription URL, syncs its nodes into `proxy_regis ```bash curl -X POST https://localhost:20128/api/v1/management/proxy-subscriptions/{id}/refresh \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -348,7 +348,7 @@ Multi-provider document OCR endpoint (Mistral OCR–compatible request and respo ```bash curl -X POST https://localhost:20128/api/v1/ocr \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -361,7 +361,7 @@ OpenAI Whisper–compatible audio translation (multipart/form-data). Unlike `/ap ```bash curl -X POST https://localhost:20128/api/v1/audio/translations \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-models/SKILL.md b/skills/omni-models/SKILL.md index a86ac9fd79..4d3b9aed0b 100644 --- a/skills/omni-models/SKILL.md +++ b/skills/omni-models/SKILL.md @@ -40,7 +40,7 @@ Create or update a model alias ```bash curl -X POST https://localhost:20128/api/models/alias \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-providers/SKILL.md b/skills/omni-providers/SKILL.md index 8a973dc354..532a134e41 100644 --- a/skills/omni-providers/SKILL.md +++ b/skills/omni-providers/SKILL.md @@ -29,7 +29,7 @@ Create provider connection ```bash curl -X POST https://localhost:20128/api/providers \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -49,7 +49,7 @@ Update provider connection ```bash curl -X PATCH https://localhost:20128/api/providers/{id} \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -69,7 +69,7 @@ Test provider connection ```bash curl -X POST https://localhost:20128/api/providers/{id}/test \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -100,7 +100,7 @@ Test multiple providers at once ```bash curl -X POST https://localhost:20128/api/providers/test-batch \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -111,7 +111,7 @@ Validate provider credentials ```bash curl -X POST https://localhost:20128/api/providers/validate \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -131,7 +131,7 @@ Import an Antigravity CLI (agy) token file as an `agy` connection ```bash curl -X POST https://localhost:20128/api/providers/agy-auth/import \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -142,7 +142,7 @@ Bulk-import multiple Antigravity CLI (agy) token files (up to 50) ```bash curl -X POST https://localhost:20128/api/providers/agy-auth/import-bulk \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -153,7 +153,7 @@ Extract `.json` token files from an uploaded ZIP for agy bulk import ```bash curl -X POST https://localhost:20128/api/providers/agy-auth/zip-extract \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -164,7 +164,7 @@ Auto-detect and import the local Antigravity CLI (agy) login from disk ```bash curl -X POST https://localhost:20128/api/providers/agy-auth/apply-local \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -184,7 +184,7 @@ Create provider node ```bash curl -X POST https://localhost:20128/api/provider-nodes \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -195,7 +195,7 @@ Update provider node ```bash curl -X PATCH https://localhost:20128/api/provider-nodes/{id} \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -215,7 +215,7 @@ Validate a provider node ```bash curl -X POST https://localhost:20128/api/provider-nodes/validate \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-settings/SKILL.md b/skills/omni-settings/SKILL.md index 9ab8f95dfb..f3d3a2ae5a 100644 --- a/skills/omni-settings/SKILL.md +++ b/skills/omni-settings/SKILL.md @@ -33,7 +33,7 @@ Update any subset of the extended memory settings. All fields are optional; only ```bash curl -X PUT https://localhost:20128/api/settings/memory \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -57,7 +57,7 @@ Update Qdrant configuration. Pass `apiKey: ""` to remove the stored key. Schema: ```bash curl -X PUT https://localhost:20128/api/settings/qdrant \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -81,7 +81,7 @@ Performs a test semantic search against the Qdrant collection. Useful for valida ```bash curl -X POST https://localhost:20128/api/settings/qdrant/search \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -94,7 +94,7 @@ Removes Qdrant points for memories that have expired or exceeded the configured ```bash curl -X POST https://localhost:20128/api/settings/qdrant/cleanup \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -125,7 +125,7 @@ Update settings ```bash curl -X PATCH https://localhost:20128/api/settings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -138,7 +138,7 @@ Deletes `call_logs`, legacy `request_detail_logs`, and local request artifact fi ```bash curl -X POST https://localhost:20128/api/settings/purge-request-history \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -158,7 +158,7 @@ Update global compression settings ```bash curl -X PUT https://localhost:20128/api/settings/compression \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -180,7 +180,7 @@ Partial-merge update. Numeric floors (e.g. a maxTextChars below the truncation-t ```bash curl -X PUT https://localhost:20128/api/settings/compression/mcp-accessibility \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -212,7 +212,7 @@ Requires a dashboard management session cookie when management auth is enabled. ```bash curl -X PUT https://localhost:20128/api/settings/payload-rules \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -241,7 +241,7 @@ Update proxy settings ```bash curl -X PATCH https://localhost:20128/api/settings/proxy \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -252,7 +252,7 @@ Test proxy connection ```bash curl -X POST https://localhost:20128/api/settings/proxy/test \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -263,7 +263,7 @@ Toggle login requirement ```bash curl -X POST https://localhost:20128/api/settings/require-login \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -288,7 +288,7 @@ Configure IP filtering with blacklist/whitelist modes, add/remove individual IPs ```bash curl -X PUT https://localhost:20128/api/settings/ip-filter \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -310,7 +310,7 @@ Update system prompt configuration ```bash curl -X PUT https://localhost:20128/api/settings/system-prompt \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -332,7 +332,7 @@ Update thinking budget configuration ```bash curl -X PUT https://localhost:20128/api/settings/thinking-budget \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -365,7 +365,7 @@ Update quota store driver settings ```bash curl -X PUT https://localhost:20128/api/settings/quota-store \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -378,7 +378,7 @@ Dashboard-only. Purges stored usage-history records. ```bash curl -X POST https://localhost:20128/api/settings/purge-usage-history \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-sync-cloud/SKILL.md b/skills/omni-sync-cloud/SKILL.md index d6d7c5d41a..68c2d4a4a1 100644 --- a/skills/omni-sync-cloud/SKILL.md +++ b/skills/omni-sync-cloud/SKILL.md @@ -22,7 +22,7 @@ Authenticates with the OmniRoute cloud worker for remote access. ```bash curl -X POST https://localhost:20128/api/cloud/auth \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -33,7 +33,7 @@ Update cloud worker credentials ```bash curl -X PUT https://localhost:20128/api/cloud/credentials/update \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -46,7 +46,7 @@ Resolves a model request through the cloud worker. ```bash curl -X POST https://localhost:20128/api/cloud/model/resolve \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -66,7 +66,7 @@ Update cloud model alias ```bash curl -X PUT https://localhost:20128/api/cloud/models/alias \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -77,7 +77,7 @@ Sync with cloud ```bash curl -X POST https://localhost:20128/api/sync/cloud \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -88,7 +88,7 @@ Initialize cloud sync ```bash curl -X POST https://localhost:20128/api/sync/initialize \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-usage-logs/SKILL.md b/skills/omni-usage-logs/SKILL.md index dd090853a7..af685e0e35 100644 --- a/skills/omni-usage-logs/SKILL.md +++ b/skills/omni-usage-logs/SKILL.md @@ -105,7 +105,7 @@ Set or update budget limits for usage tracking. ```bash curl -X POST https://localhost:20128/api/usage/budget \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/skills/omni-version-manager/SKILL.md b/skills/omni-version-manager/SKILL.md index 711a8f3e8e..eceda51dbc 100644 --- a/skills/omni-version-manager/SKILL.md +++ b/skills/omni-version-manager/SKILL.md @@ -22,7 +22,7 @@ Installs the `9router` npm package under DATA_DIR/services/9router/. Uses execFi ```bash curl -X POST https://localhost:20128/api/services/9router/install \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -35,7 +35,7 @@ Spawns the 9Router process. Idempotent if already running. **LOCAL_ONLY** — lo ```bash curl -X POST https://localhost:20128/api/services/9router/start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -48,7 +48,7 @@ Gracefully stops 9Router (SIGTERM → 15 s → SIGKILL). Idempotent. **LOCAL_ONL ```bash curl -X POST https://localhost:20128/api/services/9router/stop \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -61,7 +61,7 @@ Equivalent to stop() then start() under the operation lock. **LOCAL_ONLY** — l ```bash curl -X POST https://localhost:20128/api/services/9router/restart \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -74,7 +74,7 @@ Stops the service (if running), installs the newer npm version, then restarts. * ```bash curl -X POST https://localhost:20128/api/services/9router/update \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -87,7 +87,7 @@ Generates a new API key, encrypts it at-rest, and restarts the service to apply ```bash curl -X POST https://localhost:20128/api/services/9router/rotate-key \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -111,7 +111,7 @@ When enabled, 9Router starts automatically on the next OmniRoute boot. **LOCAL_O ```bash curl -X POST https://localhost:20128/api/services/9router/auto-start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -124,7 +124,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) 9Router process is r ```bash curl -X POST https://localhost:20128/api/services/9router/auto-restart-adopted \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -137,7 +137,7 @@ Installs the CLIProxyAPI package under DATA_DIR/services/cliproxy/. **LOCAL_ONLY ```bash curl -X POST https://localhost:20128/api/services/cliproxy/install \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -150,7 +150,7 @@ Spawns the CLIProxyAPI process. Idempotent if already running. **LOCAL_ONLY** ```bash curl -X POST https://localhost:20128/api/services/cliproxy/start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -163,7 +163,7 @@ Gracefully stops CLIProxyAPI. Idempotent. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/cliproxy/stop \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -176,7 +176,7 @@ stop() then start() under the operation lock. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/cliproxy/restart \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -189,7 +189,7 @@ Stops, installs newer version, restarts. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/cliproxy/update \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -213,7 +213,7 @@ When enabled, CLIProxyAPI starts automatically on the next OmniRoute boot. **LOC ```bash curl -X POST https://localhost:20128/api/services/cliproxy/auto-start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -226,7 +226,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) CLIProxyAPI process ```bash curl -X POST https://localhost:20128/api/services/cliproxy/auto-restart-adopted \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -239,7 +239,7 @@ Installs the `mux` npm package (coder/mux — local agent-orchestration daemon) ```bash curl -X POST https://localhost:20128/api/services/mux/install \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -252,7 +252,7 @@ Spawns `mux server --host 127.0.0.1 --port `. Idempotent if already runnin ```bash curl -X POST https://localhost:20128/api/services/mux/start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -265,7 +265,7 @@ Gracefully stops Mux. Idempotent. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/mux/stop \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -278,7 +278,7 @@ stop() then start() under the operation lock. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/mux/restart \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -291,7 +291,7 @@ Stops, installs newer version, restarts. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/mux/update \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -315,7 +315,7 @@ When enabled, Mux starts automatically on the next OmniRoute boot. **LOCAL_ONLY* ```bash curl -X POST https://localhost:20128/api/services/mux/auto-start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -328,7 +328,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) Mux process is resta ```bash curl -X POST https://localhost:20128/api/services/mux/auto-restart-adopted \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -341,7 +341,7 @@ Installs the `@maximhq/bifrost` npm package under DATA_DIR/services/bifrost/. Th ```bash curl -X POST https://localhost:20128/api/services/bifrost/install \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -354,7 +354,7 @@ Starts the supervised Bifrost process. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/bifrost/start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -367,7 +367,7 @@ Stops the supervised Bifrost process. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/bifrost/stop \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -380,7 +380,7 @@ Restarts the supervised Bifrost process. **LOCAL_ONLY** — loopback only. ```bash curl -X POST https://localhost:20128/api/services/bifrost/restart \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -393,7 +393,7 @@ Updates Bifrost to the latest npm version. Stops the running process, installs t ```bash curl -X POST https://localhost:20128/api/services/bifrost/update \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -417,7 +417,7 @@ When enabled, Bifrost starts automatically on the next OmniRoute boot. **LOCAL_O ```bash curl -X POST https://localhost:20128/api/services/bifrost/auto-start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -430,7 +430,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) Bifrost process is r ```bash curl -X POST https://localhost:20128/api/services/bifrost/auto-restart-adopted \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -443,7 +443,7 @@ Installs the `@askalf/dario` npm package (Claude-account-pool proxy) under DATA_ ```bash curl -X POST https://localhost:20128/api/services/dario/install \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -456,7 +456,7 @@ Spawns the Dario process. Idempotent if already running. **LOCAL_ONLY** — loop ```bash curl -X POST https://localhost:20128/api/services/dario/start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -469,7 +469,7 @@ Gracefully stops Dario. Idempotent — returns a stopped status even if no super ```bash curl -X POST https://localhost:20128/api/services/dario/stop \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -482,7 +482,7 @@ Equivalent to stop() then start() under the operation lock. **LOCAL_ONLY** — l ```bash curl -X POST https://localhost:20128/api/services/dario/restart \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -495,7 +495,7 @@ Stops the service (if running), installs the newer npm version, then restarts it ```bash curl -X POST https://localhost:20128/api/services/dario/update \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -519,7 +519,7 @@ When enabled, Dario starts automatically on the next OmniRoute boot. **LOCAL_ONL ```bash curl -X POST https://localhost:20128/api/services/dario/auto-start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -532,7 +532,7 @@ When enabled, an externally-adopted (not OmniRoute-spawned) Dario process is res ```bash curl -X POST https://localhost:20128/api/services/dario/auto-restart-adopted \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -545,7 +545,7 @@ Forwards to the running Dario instance's `POST /admin/login/start` using the sto ```bash curl -X POST https://localhost:20128/api/services/dario/admin/login-start \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -558,7 +558,7 @@ Forwards to the running Dario instance's `POST /admin/login/complete`. On succes ```bash curl -X POST https://localhost:20128/api/services/dario/admin/login-complete \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -604,7 +604,7 @@ Writes the source connection's access/refresh token pair directly into Dario's o ```bash curl -X POST https://localhost:20128/api/services/dario/admin/import-from-omniroute \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts index 2fa930dd7d..3c662c0f78 100644 --- a/src/app/api/settings/route.ts +++ b/src/app/api/settings/route.ts @@ -35,6 +35,7 @@ import { AUTHZ_HEADER_AUTH_KIND, AUTHZ_HEADER_PEER_LOCALITY, } from "@/server/authz/headers"; +import { readSubjectFromHeaders } from "@/server/authz/assertAuth"; /** * Force this route to run dynamically per-request and never be cached/prerendered. @@ -133,6 +134,8 @@ async function deriveAuditActor(request: Request): Promise { } catch { /* fall through */ } + const subject = readSubjectFromHeaders(request.headers); + if (subject.kind === "management_key" && subject.label === "local-cli-token") return "cli"; try { if (await isCliTokenAuthValid(request)) return "cli"; } catch { diff --git a/src/lib/agentSkills/generator.ts b/src/lib/agentSkills/generator.ts index dd106b42ba..8f5599622f 100644 --- a/src/lib/agentSkills/generator.ts +++ b/src/lib/agentSkills/generator.ts @@ -76,6 +76,7 @@ function extractCustomBlock(content: string): string | null { function buildApiBody(skill: AgentSkill, sources: BuildSources): string { const areaMap = sources.openapi.areas; const ops = areaMap.get(skill.area as Parameters[0]) ?? []; + const usesDashboardSession = skill.id === "omni-auth"; const lines: string[] = []; @@ -84,10 +85,17 @@ function buildApiBody(skill: AgentSkill, sources: BuildSources): string { lines.push(""); lines.push("## Authentication\n"); - lines.push( - "All requests require a valid Bearer token or session cookie. " + - "Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development." - ); + if (usesDashboardSession) { + lines.push( + "Remote API requests use a Bearer credential. Dashboard login is different: " + + "`POST /api/auth/login` accepts a management password and returns an `auth_token` session cookie." + ); + } else { + lines.push( + "All requests require a valid Bearer token or session cookie. " + + "Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development." + ); + } lines.push(""); lines.push("## Endpoints\n"); @@ -105,14 +113,41 @@ function buildApiBody(skill: AgentSkill, sources: BuildSources): string { lines.push(op.description); lines.push(""); } - // Minimal curl example - const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `; + // Minimal curl example. Only omni-auth establishes and consumes a dashboard + // session; generic API skills use independently usable Bearer examples. lines.push("```bash"); - lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`); - lines.push(' -H "Authorization: Bearer $OMNIROUTE_TOKEN"'); - if (["POST", "PUT", "PATCH"].includes(op.method)) { + if (usesDashboardSession && op.path === "/api/auth/login" && op.method === "POST") { + lines.push(`curl -X POST https://localhost:20128${op.path} \\`); lines.push(' -H "Content-Type: application/json" \\'); - lines.push(" -d '{}'"); + lines.push(" -c cookie.jar \\"); + lines.push(' -d \'{"password":""}\''); + } else if (usesDashboardSession) { + const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `; + if (op.method === "GET") { + lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`); + lines.push(" -b cookie.jar"); + } else { + lines.push( + "CSRF_TOKEN=$(curl -s https://localhost:20128/api/auth/csrf -b cookie.jar | jq -r .token)" + ); + lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`); + lines.push(" -b cookie.jar \\"); + const hasJsonBody = ["POST", "PUT", "PATCH"].includes(op.method); + lines.push(` -H "x-omniroute-csrf: $CSRF_TOKEN"${hasJsonBody ? " \\" : ""}`); + if (hasJsonBody) { + lines.push(' -H "Content-Type: application/json" \\'); + lines.push(" -d '{}'"); + } + } + } else { + const curlMethod = op.method === "GET" ? "" : `-X ${op.method} `; + const hasJsonBody = ["POST", "PUT", "PATCH"].includes(op.method); + lines.push(`curl ${curlMethod}https://localhost:20128${op.path} \\`); + lines.push(` -H "Authorization: Bearer $OMNIROUTE_TOKEN"${hasJsonBody ? " \\" : ""}`); + if (hasJsonBody) { + lines.push(' -H "Content-Type: application/json" \\'); + lines.push(" -d '{}'"); + } } lines.push("```"); lines.push(""); diff --git a/src/lib/api/requireManagementAuth.ts b/src/lib/api/requireManagementAuth.ts index eb0dc99239..34ebc25cbf 100644 --- a/src/lib/api/requireManagementAuth.ts +++ b/src/lib/api/requireManagementAuth.ts @@ -5,6 +5,7 @@ import { getApiKeyMetadata } from "@/lib/db/apiKeys"; import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth"; import { evaluateAccessTokenAuth } from "@/server/authz/accessTokenAuth"; import { isTrustedLoopbackInternalServiceRequest } from "@/lib/api/internalServiceAuth"; +import { AUTHZ_HEADER_AUTH_KIND, AUTHZ_HEADER_AUTH_LABEL } from "@/server/authz/headers"; import { MANAGE_SCOPE, hasManageScope as hasManageScopeShared, @@ -52,7 +53,17 @@ export async function requireManagementAuth( return null; } - // CLI machine-id token allows localhost CLI access without an explicit API key. + // The authz pipeline strips the raw machine-token header after it validates it + // and forwards this trusted subject stamp to route handlers. + if ( + request.headers.get(AUTHZ_HEADER_AUTH_KIND) === "management_key" && + request.headers.get(AUTHZ_HEADER_AUTH_LABEL) === "local-cli-token" + ) { + return null; + } + + // Direct/raw-Node callers without the central pipeline can still validate the + // CLI token here, including the trusted peer-locality stamp path. if (await isCliTokenAuthValid(request)) { return null; } diff --git a/src/lib/machineToken.ts b/src/lib/machineToken.ts index e83da29495..bb910ae9ed 100644 --- a/src/lib/machineToken.ts +++ b/src/lib/machineToken.ts @@ -1,9 +1,13 @@ import { createHash, createHmac } from "node:crypto"; +import { createRequire } from "node:module"; let machineIdSync: (original?: boolean) => string; try { - // Use require() to bypass webpack static analysis that breaks the default export - const mod = require("node-machine-id"); + // Anchor runtime resolution to the process entrypoint. Turbopack rewrites + // createRequire(import.meta.url) into an in-bundle resolver, which cannot load + // external CommonJS packages from the installed standalone node_modules tree. + const runtimeRequire = createRequire(process.argv[1] || process.cwd()); + const mod = runtimeRequire("node-machine-id"); machineIdSync = mod.machineIdSync || mod.default?.machineIdSync; } catch { machineIdSync = () => ""; @@ -15,10 +19,19 @@ function getActiveSalt(): string { return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT; } -function deriveToken(rawId: string, salt: string): string { +export function deriveMachineToken(rawId: string, salt: string): string { + if (!rawId) return ""; return createHmac("sha256", rawId).update(salt).digest("hex"); } +export function deriveLegacyCliToken(machineId: string, salt: string): string { + if (!machineId) return ""; + return createHash("sha256") + .update(machineId + salt) + .digest("hex") + .substring(0, 32); +} + let cached: string | null = null; let cachedSalt: string | null = null; @@ -27,8 +40,9 @@ export function getMachineTokenSync(salt?: string): string { try { // machineIdSync(true) returns the original unhashed hardware ID. const rawId = machineIdSync(true); + if (!rawId) return ""; if (activeSalt === cachedSalt && cached !== null) return cached; - const token = deriveToken(rawId, activeSalt); + const token = deriveMachineToken(rawId, activeSalt); if (!salt) { cached = token; cachedSalt = activeSalt; @@ -43,10 +57,7 @@ export function getLegacyCliTokenSync(salt?: string): string { const activeSalt = salt ?? getActiveSalt(); try { const machineId = machineIdSync(); - return createHash("sha256") - .update(machineId + activeSalt) - .digest("hex") - .substring(0, 32); + return deriveLegacyCliToken(machineId, activeSalt); } catch { return ""; } diff --git a/src/server/authz/pipeline.ts b/src/server/authz/pipeline.ts index f2619a189b..9f4e46bed1 100644 --- a/src/server/authz/pipeline.ts +++ b/src/server/authz/pipeline.ts @@ -26,6 +26,7 @@ import { AUTHZ_HEADER_REQUEST_ID, AUTHZ_HEADER_ROUTE_CLASS, AUTHZ_TRUSTED_HEADERS, + CLI_TOKEN_HEADER, PEER_IP_HEADER, VIA_PROXY_HEADER, } from "./headers"; @@ -330,6 +331,11 @@ export async function runAuthzPipeline( process.env.OMNIROUTE_PEER_STAMP_TOKEN ); requestHeaders.set(AUTHZ_HEADER_PEER_LOCALITY, peerLocality); + // Local CLI-token auth is decided centrally above. Preserve that trusted + // decision for route-level requireManagementAuth without forwarding the + // machine token itself: custom client auth headers are stripped before the + // route runs, so the route consumes only the stamped auth subject. + requestHeaders.delete(CLI_TOKEN_HEADER); if (method === "OPTIONS") { const preflight = new NextResponse(null, { status: 204 }); diff --git a/src/server/authz/policies/management.ts b/src/server/authz/policies/management.ts index e3523035f6..772c801247 100644 --- a/src/server/authz/policies/management.ts +++ b/src/server/authz/policies/management.ts @@ -77,6 +77,7 @@ function isPrivateLanRequest(ctx: PolicyContext): boolean { } function hasValidCliToken(ctx: PolicyContext): boolean { + if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") return false; if (!isLoopbackRequest(ctx)) return false; const headers = ctx.request.headers; const provided = headers.get(CLI_TOKEN_HEADER); diff --git a/tests/unit/agentSkills-generator.test.ts b/tests/unit/agentSkills-generator.test.ts index df0f5d8d47..9391af4a7f 100644 --- a/tests/unit/agentSkills-generator.test.ts +++ b/tests/unit/agentSkills-generator.test.ts @@ -15,13 +15,10 @@ import os from "node:os"; // ── Dynamic imports (tsx/esm resolves TS imports) ──────────────────────────── -const { generateAgentSkills, buildSkillMarkdown, __testing } = await import( - "../../src/lib/agentSkills/generator.ts" -); +const { generateAgentSkills, buildSkillMarkdown, __testing } = + await import("../../src/lib/agentSkills/generator.ts"); -const { getCatalog, refreshCatalog } = await import( - "../../src/lib/agentSkills/catalog.ts" -); +const { getCatalog, refreshCatalog } = await import("../../src/lib/agentSkills/catalog.ts"); // ── Helpers ────────────────────────────────────────────────────────────────── @@ -66,7 +63,7 @@ test("dry-run (default) returns report without writing any files", async () => { assert.equal( report.generated.length + report.unchanged.length, 46, - `Expected 46 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`, + `Expected 46 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}` ); assert.equal(report.errors.length, 0, `Unexpected errors: ${JSON.stringify(report.errors)}`); @@ -75,7 +72,7 @@ test("dry-run (default) returns report without writing any files", async () => { assert.equal( entries.length, 0, - `Dry-run wrote ${entries.length} entries to ${tmpDir}: ${entries.join(", ")}`, + `Dry-run wrote ${entries.length} entries to ${tmpDir}: ${entries.join(", ")}` ); } finally { rmTmpDir(tmpDir); @@ -128,7 +125,7 @@ test("apply mode writes SKILL.md with valid frontmatter for omni-providers", asy // Generated comment present assert.ok( content.includes("\nMy custom content here.\n"; + const customBlock = + "\nMy custom content here.\n"; const contentWithCustom = originalContent + "\n" + customBlock + "\n"; fs.writeFileSync(skillFile, contentWithCustom, "utf-8"); @@ -355,27 +410,17 @@ test("marker preservation: custom block survives regeneration", async () => { onlyIds: ["omni-providers"], }); - assert.equal( - report2.errors.length, - 0, - `Errors: ${JSON.stringify(report2.errors)}`, - ); + assert.equal(report2.errors.length, 0, `Errors: ${JSON.stringify(report2.errors)}`); const newContent = fs.readFileSync(skillFile, "utf-8"); // Custom block should still be present assert.ok( newContent.includes("My custom content here."), - "Custom content was lost during regeneration", - ); - assert.ok( - newContent.includes(""), - "Custom start marker missing", - ); - assert.ok( - newContent.includes(""), - "Custom end marker missing", + "Custom content was lost during regeneration" ); + assert.ok(newContent.includes(""), "Custom start marker missing"); + assert.ok(newContent.includes(""), "Custom end marker missing"); } finally { rmTmpDir(tmpDir); } @@ -391,7 +436,10 @@ test("buildSkillMarkdown returns valid frontmatter + body for omni-providers", ( assert.ok(typeof result.frontmatter === "object", "frontmatter must be an object"); assert.equal(result.frontmatter.name, "omni-providers"); assert.ok(result.frontmatter.description.length > 0, "description must be non-empty"); - assert.ok(typeof result.body === "string" && result.body.length > 0, "body must be a non-empty string"); + assert.ok( + typeof result.body === "string" && result.body.length > 0, + "body must be a non-empty string" + ); }); test("buildSkillMarkdown body has no erroneously escaped characters", () => { @@ -406,20 +454,11 @@ test("buildSkillMarkdown body has no erroneously escaped characters", () => { // Check for common escape errors: \\n in rendered text, &, <, > assert.ok( !result.body.includes("\\\\n"), - `Skill ${id}: body contains \\\\n (double-escaped newline)`, - ); - assert.ok( - !result.body.includes("&"), - `Skill ${id}: body contains HTML entity &`, - ); - assert.ok( - !result.body.includes("<"), - `Skill ${id}: body contains HTML entity <`, - ); - assert.ok( - !result.body.includes(">"), - `Skill ${id}: body contains HTML entity >`, + `Skill ${id}: body contains \\\\n (double-escaped newline)` ); + assert.ok(!result.body.includes("&"), `Skill ${id}: body contains HTML entity &`); + assert.ok(!result.body.includes("<"), `Skill ${id}: body contains HTML entity <`); + assert.ok(!result.body.includes(">"), `Skill ${id}: body contains HTML entity >`); } }); @@ -430,7 +469,7 @@ test("buildSkillMarkdown throws for unknown skillId", () => { assert.throws( () => buildSkillMarkdown("non-existent-skill", sources), /non-existent-skill/, - "Should throw with skill ID in message", + "Should throw with skill ID in message" ); }); @@ -464,7 +503,7 @@ test("buildSkillMarkdown description is at most 2000 chars", () => { const result = buildSkillMarkdown(skill.id, sources); assert.ok( result.frontmatter.description.length <= 2000, - `Skill ${skill.id}: description too long (${result.frontmatter.description.length} > 2000)`, + `Skill ${skill.id}: description too long (${result.frontmatter.description.length} > 2000)` ); } }); @@ -509,8 +548,10 @@ test("generated SKILL.md contains the mandatory generated comment", async () => const content = fs.readFileSync(path.join(tmpDir, "omni-providers", "SKILL.md"), "utf-8"); assert.ok( - content.includes(""), - "Missing mandatory generated comment", + content.includes( + "" + ), + "Missing mandatory generated comment" ); } finally { rmTmpDir(tmpDir); diff --git a/tests/unit/api/settings-audit.test.ts b/tests/unit/api/settings-audit.test.ts index e23296713b..20844c741a 100644 --- a/tests/unit/api/settings-audit.test.ts +++ b/tests/unit/api/settings-audit.test.ts @@ -108,6 +108,28 @@ test("AC-9: successful PATCH writes settings.update with diff of changed keys", }); }); +test("CLI subject stamp preserves actor attribution after the raw token is stripped", async () => { + await bootstrapWithPassword("initial-pass-cli-actor"); + await settingsDb.updateSettings({ theme: "light" }); + + const response = await settingsRoute.PATCH( + new Request("http://localhost/api/settings", { + method: "PATCH", + headers: { + "content-type": "application/json", + "x-omniroute-auth-kind": "management_key", + "x-omniroute-auth-label": "local-cli-token", + }, + body: JSON.stringify({ theme: "dark" }), + }) + ); + + assert.equal(response.status, 200); + const rows = settingsRows().filter((r) => r.action === "settings.update"); + assert.equal(rows.length, 1); + assert.equal(rows[0].actor, "cli"); +}); + // ─── AC-10 — failure rows for each rejection path ──────────────────────── test("AC-10a: PASSWORD_REQUIRED failure writes settings.update_failed", async () => { diff --git a/tests/unit/check-pack-boot.test.ts b/tests/unit/check-pack-boot.test.ts index ea592db217..56abe03176 100644 --- a/tests/unit/check-pack-boot.test.ts +++ b/tests/unit/check-pack-boot.test.ts @@ -1,14 +1,18 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import { createHmac } from "node:crypto"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { REQUIRED_SQLJS_RUNTIME_FILES, + REQUIRED_MACHINE_TOKEN_RUNTIME_FILES, pickTarball, evaluateBoot, pickPort, findMissingSqlJsRuntimeFiles, + findMissingMachineTokenRuntimeFiles, + evaluateMachineTokenAuth, evaluateSqlJsRoundTrip, evaluateRestartPersistence, } from "../../scripts/check/check-pack-boot.mjs"; @@ -74,6 +78,64 @@ test("installed package contract requires sql.js metadata, entrypoint, and WASM" ); }); +test("installed package contract requires a resolvable node-machine-id CommonJS runtime", () => { + const present = new Set( + REQUIRED_MACHINE_TOKEN_RUNTIME_FILES.map((file) => path.join("/pkg", file)) + ); + assert.deepEqual( + findMissingMachineTokenRuntimeFiles("/pkg", (file) => present.has(file)), + [] + ); + + present.delete(path.join("/pkg", "node_modules/node-machine-id/index.js")); + assert.deepEqual( + findMissingMachineTokenRuntimeFiles("/pkg", (file) => present.has(file)), + ["node_modules/node-machine-id/index.js"] + ); +}); + +test("machine-token smoke requires no/invalid credentials to fail and the packaged CLI token to pass", () => { + assert.deepEqual( + evaluateMachineTokenAuth({ + cliToken: "a".repeat(64), + unauthenticatedStatus: 401, + invalidStatus: 401, + authenticatedStatus: 200, + }), + { ok: true, failures: [] } + ); + + for (const candidate of [ + { cliToken: "", unauthenticatedStatus: 401, invalidStatus: 401, authenticatedStatus: 200 }, + { + cliToken: "a".repeat(64), + unauthenticatedStatus: 200, + invalidStatus: 401, + authenticatedStatus: 200, + }, + { + cliToken: "a".repeat(64), + unauthenticatedStatus: 401, + invalidStatus: 200, + authenticatedStatus: 200, + }, + { + cliToken: "a".repeat(64), + unauthenticatedStatus: 401, + invalidStatus: 401, + authenticatedStatus: 401, + }, + { + cliToken: createHmac("sha256", "").update("omniroute-cli-auth-v1").digest("hex"), + unauthenticatedStatus: 401, + invalidStatus: 401, + authenticatedStatus: 200, + }, + ]) { + assert.equal(evaluateMachineTokenAuth(candidate).ok, false); + } +}); + test("sql.js round trip requires the forced-driver marker plus PATCH and GET persistence", () => { const passing = evaluateSqlJsRoundTrip({ startupOutput: "[DB] Pre-initializing sql.js WASM (synchronous drivers unavailable)...", @@ -103,10 +165,20 @@ test("source guard: the gate polls the real health endpoint of the INSTALLED bin ); assert.ok(src.includes("/api/monitoring/health"), "must poll the health endpoint"); assert.ok(src.includes("/api/settings"), "must verify a real application write and read"); + assert.ok(src.includes("/api/cli/whoami"), "must exercise the machine-token auth endpoint"); + assert.ok(src.includes("x-omniroute-cli-token"), "must send the official machine-token header"); + const postinstall = readFileSync( + fileURLToPath(new URL("../../scripts/build/postinstall.mjs", import.meta.url)), + "utf8" + ); + assert.ok(postinstall.includes('["sql.js", "node-machine-id"]')); + assert.ok(postinstall.includes('join(ROOT, "dist", "node_modules", packageName)')); assert.ok( src.includes('OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1"'), "must force the packaged sql.js tier during this smoke" ); + assert.ok(src.includes("MAX_SERVER_OUTPUT_CHARS")); + assert.ok(!src.includes("while (tail.length > 80)"), "must not discard early startup proof"); assert.ok(src.indexOf("npm") < src.indexOf("spawn"), "pack+install must precede the boot spawn"); }); diff --git a/tests/unit/cli-doctor-command.test.ts b/tests/unit/cli-doctor-command.test.ts index c10aa53256..4eedec0fdd 100644 --- a/tests/unit/cli-doctor-command.test.ts +++ b/tests/unit/cli-doctor-command.test.ts @@ -15,6 +15,8 @@ const ORIGINAL_STORAGE_ENCRYPTION_KEY = process.env.STORAGE_ENCRYPTION_KEY; interface DoctorCheck { name: string; status: string; + message?: string; + details?: Record; } interface DoctorResult { @@ -109,3 +111,162 @@ test("doctor fails when encrypted credentials exist without storage key", async assert.equal(getCheck(result, "Storage/encryption")?.status, "fail"); }); }); + +test("doctor probes the real machine-token endpoint without exposing the token", async () => { + await withDoctorEnv(async () => { + const originalFetch = globalThis.fetch; + let observedToken = ""; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const url = String(input); + assert.match(url, /\/api\/cli\/whoami$/); + assert.equal(init?.redirect, "error"); + observedToken = new Headers(init?.headers).get("x-omniroute-cli-token") || ""; + return new Response(JSON.stringify({ authenticated: true }), { + status: observedToken ? 200 : 401, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + try { + const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs"); + const check = await checkMachineTokenAuth({ + livenessUrl: "http://127.0.0.1:21999/api/health/degradation", + }); + + assert.equal(check.status, "ok"); + assert.match(observedToken, /^[0-9a-f]{64}$/); + assert.ok( + !JSON.stringify(check).includes(observedToken), + "doctor output must never expose token" + ); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); + +test("doctor only sends the machine token to supported loopback URL shapes", async () => { + const originalFetch = globalThis.fetch; + const observedUrls: string[] = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + observedUrls.push(String(input)); + assert.equal(init?.redirect, "error"); + assert.match(new Headers(init?.headers).get("x-omniroute-cli-token") || "", /^[0-9a-f]{64}$/); + return new Response(null, { status: 200 }); + }) as typeof fetch; + + try { + const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs"); + const loopbackUrls = [ + "http://localhost:21999/health", + "http://127.0.0.42:21999/health", + "http://[::1]:21999/health", + "http://[::ffff:127.0.0.1]:21999/health", + ]; + + for (const livenessUrl of loopbackUrls) { + const check = await checkMachineTokenAuth({ livenessUrl }); + assert.equal(check.status, "ok", livenessUrl); + } + assert.equal(observedUrls.length, loopbackUrls.length); + assert.ok(observedUrls.every((url) => url.endsWith("/api/cli/whoami"))); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("doctor refuses remote, deceptive, credential-bearing, and unsupported probe URLs", async () => { + const originalFetch = globalThis.fetch; + let fetchCalls = 0; + globalThis.fetch = (async () => { + fetchCalls += 1; + return new Response(null, { status: 200 }); + }) as typeof fetch; + + try { + const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs"); + const rejectedUrls = [ + "https://remote.example.test/health", + "http://localhost.example.test/health", + "http://127.0.0.1.example.test/health", + "http://localhost@remote.example.test/health", + "http://token-user:credential-sentinel@127.0.0.1:21999/health", + "ftp://localhost:21999/health", + "http://0.0.0.0:21999/health", + "http://[::2]:21999/health", + ]; + + for (const livenessUrl of rejectedUrls) { + const check = await checkMachineTokenAuth({ livenessUrl }); + assert.equal(check.status, "warn", livenessUrl); + assert.equal(check.details?.accepted, false); + assert.equal(check.details?.tokenExposed, false); + assert.ok(!JSON.stringify(check).includes("credential-sentinel")); + } + assert.equal(fetchCalls, 0, "rejected targets must never receive a fetch call"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("doctor never follows a machine-token redirect to another origin", async () => { + const originalFetch = globalThis.fetch; + let crossOriginRequests = 0; + let crossOriginTokenObserved = false; + globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => { + if (init?.redirect !== "error") { + crossOriginRequests += 1; + crossOriginTokenObserved = new Headers(init?.headers).has("x-omniroute-cli-token"); + return new Response(null, { status: 200 }); + } + throw new TypeError("redirect blocked"); + }) as typeof fetch; + + try { + const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs"); + const check = await checkMachineTokenAuth({ + livenessUrl: "http://127.0.0.1:21999/redirect-to-other-origin", + }); + + assert.equal(check.status, "warn"); + assert.equal(crossOriginRequests, 0); + assert.equal(crossOriginTokenObserved, false); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("doctor gives connect guidance when the server rejects a machine token", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response(null, { status: 401 })) as typeof fetch; + try { + const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs"); + const check = await checkMachineTokenAuth({ + livenessUrl: "http://127.0.0.1:21999/api/health/degradation", + }); + assert.equal(check.status, "warn"); + assert.match(check.message || "", /omniroute connect/i); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("doctor reports explicitly disabled machine-token auth without probing", async () => { + const previous = process.env.OMNIROUTE_DISABLE_CLI_TOKEN; + const originalFetch = globalThis.fetch; + process.env.OMNIROUTE_DISABLE_CLI_TOKEN = "true"; + globalThis.fetch = (async () => { + throw new Error("fetch should not run"); + }) as typeof fetch; + try { + const { checkMachineTokenAuth } = await import("../../bin/cli/commands/doctor.mjs"); + const check = await checkMachineTokenAuth(); + assert.equal(check.status, "warn"); + assert.equal(check.details?.disabled, true); + assert.match(check.message || "", /disabled/i); + } finally { + globalThis.fetch = originalFetch; + if (previous === undefined) delete process.env.OMNIROUTE_DISABLE_CLI_TOKEN; + else process.env.OMNIROUTE_DISABLE_CLI_TOKEN = previous; + } +}); diff --git a/tests/unit/cli-machine-token.test.ts b/tests/unit/cli-machine-token.test.ts index 833bf67a0c..35170f5a36 100644 --- a/tests/unit/cli-machine-token.test.ts +++ b/tests/unit/cli-machine-token.test.ts @@ -1,6 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import crypto from "node:crypto"; +import http from "node:http"; import { execFileSync } from "node:child_process"; import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -12,6 +13,37 @@ test("cliToken.mjs pode ser importado sem erro", async () => { assert.equal(mod.CLI_TOKEN_HEADER, "x-omniroute-cli-token"); }); +test("packaged CLI derives the same current machine token as the server", async () => { + const salt = `cli-machine-token-${process.pid}`; + const previousSalt = process.env.OMNIROUTE_CLI_SALT; + process.env.OMNIROUTE_CLI_SALT = salt; + try { + const { getCliToken } = await import(`../../bin/cli/utils/cliToken.mjs?current=${Date.now()}`); + const { getMachineTokenSync } = await import("../../src/lib/machineToken.ts"); + const token = await getCliToken(); + + assert.match(token, /^[0-9a-f]{64}$/, "CLI token must be a non-empty HMAC-SHA256 digest"); + assert.equal(token, getMachineTokenSync(salt)); + } finally { + if (previousSalt === undefined) delete process.env.OMNIROUTE_CLI_SALT; + else process.env.OMNIROUTE_CLI_SALT = previousSalt; + } +}); + +test("getCliToken returns an empty string when machine-id derivation is unavailable", async () => { + const { deriveCliToken } = await import("../../bin/cli/utils/cliToken.mjs"); + assert.equal(deriveCliToken({}, "test-salt"), ""); + assert.equal(deriveCliToken({ default: { machineIdSync: () => "" } }, "test-salt"), ""); + const throwingModule = { + default: { + machineIdSync: () => { + throw new Error("unavailable"); + }, + }, + }; + assert.equal(deriveCliToken(throwingModule, "test-salt"), ""); +}); + test("getCliToken retorna string de 64 chars ou string vazia", async () => { const { getCliToken } = await import("../../bin/cli/utils/cliToken.mjs"); const token = await getCliToken(); @@ -97,6 +129,124 @@ test("OMNIROUTE_CLI_TOKEN env sobrescreve token gerado em apiFetch", async () => } }); +test("apiFetch never sends an implicit machine token to remote contexts", async () => { + const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; + const originalOverride = process.env.OMNIROUTE_CLI_TOKEN; + process.env.OMNIROUTE_BASE_URL = "https://remote.example.test"; + delete process.env.OMNIROUTE_CLI_TOKEN; + try { + const { buildHeaders } = await import(`../../bin/cli/api.mjs?remote=${Date.now()}`); + const headers = await buildHeaders({}); + assert.equal(headers.has("x-omniroute-cli-token"), false); + } finally { + if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; + else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; + if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN; + else process.env.OMNIROUTE_CLI_TOKEN = originalOverride; + } +}); + +test("apiFetch sends the implicit machine token only to loopback destinations", async () => { + const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; + const originalOverride = process.env.OMNIROUTE_CLI_TOKEN; + process.env.OMNIROUTE_BASE_URL = "http://127.0.0.1:20128"; + delete process.env.OMNIROUTE_CLI_TOKEN; + try { + const [{ buildHeaders, isLoopbackUrl }, { getCliToken }] = await Promise.all([ + import(`../../bin/cli/api.mjs?loopback=${Date.now()}`), + import("../../bin/cli/utils/cliToken.mjs"), + ]); + assert.equal(isLoopbackUrl("http://localhost:20128"), true); + assert.equal(isLoopbackUrl("http://127.0.0.42:20128"), true); + assert.equal(isLoopbackUrl("http://[::1]:20128"), true); + assert.equal(isLoopbackUrl("https://remote.example.test"), false); + const headers = await buildHeaders({}); + assert.equal(headers.get("x-omniroute-cli-token"), await getCliToken()); + } finally { + if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; + else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; + if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN; + else process.env.OMNIROUTE_CLI_TOKEN = originalOverride; + } +}); + +test("CLI-token overrides are also suppressed for remote contexts", async () => { + const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; + const originalOverride = process.env.OMNIROUTE_CLI_TOKEN; + process.env.OMNIROUTE_BASE_URL = "https://remote.example.test"; + process.env.OMNIROUTE_CLI_TOKEN = "must-not-leave-loopback"; + try { + const { buildHeaders } = await import(`../../bin/cli/api.mjs?override=${Date.now()}`); + const headers = await buildHeaders({ cliToken: "also-local-only" }); + assert.equal(headers.has("x-omniroute-cli-token"), false); + } finally { + if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; + else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; + if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN; + else process.env.OMNIROUTE_CLI_TOKEN = originalOverride; + } +}); + +test("absolute remote URLs cannot inherit a local context machine token", async () => { + const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; + const originalOverride = process.env.OMNIROUTE_CLI_TOKEN; + const originalFetch = globalThis.fetch; + process.env.OMNIROUTE_BASE_URL = "http://127.0.0.1:20128"; + process.env.OMNIROUTE_CLI_TOKEN = "must-stay-local"; + let receivedHeaders: Headers | null = null; + globalThis.fetch = (async (_url, init) => { + receivedHeaders = new Headers(init?.headers); + return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + try { + const { apiFetch } = await import(`../../bin/cli/api.mjs?absolute=${Date.now()}`); + await apiFetch("https://remote.example.test/probe", { retry: false }); + assert.equal(receivedHeaders?.has("x-omniroute-cli-token"), false); + } finally { + globalThis.fetch = originalFetch; + if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; + else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; + if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN; + else process.env.OMNIROUTE_CLI_TOKEN = originalOverride; + } +}); + +test("apiFetch refuses redirects while carrying a local machine token", async () => { + const originalBaseUrl = process.env.OMNIROUTE_BASE_URL; + const originalOverride = process.env.OMNIROUTE_CLI_TOKEN; + let redirectedRequests = 0; + const destination = http.createServer((_request, response) => { + redirectedRequests += 1; + response.end("unexpected"); + }); + const redirector = http.createServer((_request, response) => { + const destinationAddress = destination.address(); + assert.ok(destinationAddress && typeof destinationAddress === "object"); + response.writeHead(302, { location: `http://127.0.0.1:${destinationAddress.port}/target` }); + response.end(); + }); + await new Promise((resolve) => destination.listen(0, "127.0.0.1", resolve)); + await new Promise((resolve) => redirector.listen(0, "127.0.0.1", resolve)); + const redirectorAddress = redirector.address(); + assert.ok(redirectorAddress && typeof redirectorAddress === "object"); + process.env.OMNIROUTE_BASE_URL = `http://127.0.0.1:${redirectorAddress.port}`; + process.env.OMNIROUTE_CLI_TOKEN = "redirect-secret"; + try { + const { apiFetch } = await import(`../../bin/cli/api.mjs?redirect=${Date.now()}`); + await assert.rejects(() => apiFetch("/redirect", { retry: false }), /fetch failed/i); + assert.equal(redirectedRequests, 0); + } finally { + await Promise.all([ + new Promise((resolve) => redirector.close(() => resolve())), + new Promise((resolve) => destination.close(() => resolve())), + ]); + if (originalBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL; + else process.env.OMNIROUTE_BASE_URL = originalBaseUrl; + if (originalOverride === undefined) delete process.env.OMNIROUTE_CLI_TOKEN; + else process.env.OMNIROUTE_CLI_TOKEN = originalOverride; + } +}); + // --- testes server-side: isLoopback --- test("isLoopback aceita 127.0.0.1", async () => { diff --git a/tests/unit/lib/machineToken.test.ts b/tests/unit/lib/machineToken.test.ts index addd7dc7ae..003780e15d 100644 --- a/tests/unit/lib/machineToken.test.ts +++ b/tests/unit/lib/machineToken.test.ts @@ -1,6 +1,15 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { getMachineTokenSync } from "../../../src/lib/machineToken.ts"; +import { + deriveLegacyCliToken, + deriveMachineToken, + getMachineTokenSync, +} from "../../../src/lib/machineToken.ts"; + +test("machine-token derivation fails closed for a missing machine ID", () => { + assert.equal(deriveMachineToken("", "omniroute-cli-auth-v1"), ""); + assert.equal(deriveLegacyCliToken("", "omniroute-cli-auth-v1"), ""); +}); test("getMachineTokenSync returns a 64-character hex string (full SHA-256)", () => { const token = getMachineTokenSync(); @@ -22,10 +31,15 @@ test("getMachineTokenSync with empty string salt does not throw", () => { }); test("getMachineTokenSync respects OMNIROUTE_CLI_SALT env var", () => { - const before = getMachineTokenSync(); - process.env.OMNIROUTE_CLI_SALT = "__test_salt__"; - const withEnv = getMachineTokenSync(); - delete process.env.OMNIROUTE_CLI_SALT; - assert.notEqual(before, withEnv, "env salt must produce a different token"); - assert.match(withEnv, /^[0-9a-f]{64}$/, "env-derived token must still be 64-char hex"); + const previous = process.env.OMNIROUTE_CLI_SALT; + try { + const before = getMachineTokenSync(); + process.env.OMNIROUTE_CLI_SALT = "__test_salt__"; + const withEnv = getMachineTokenSync(); + assert.notEqual(before, withEnv, "env salt must produce a different token"); + assert.match(withEnv, /^[0-9a-f]{64}$/, "env-derived token must still be 64-char hex"); + } finally { + if (previous === undefined) delete process.env.OMNIROUTE_CLI_SALT; + else process.env.OMNIROUTE_CLI_SALT = previous; + } }); diff --git a/tests/unit/lib/managementCliToken.test.ts b/tests/unit/lib/managementCliToken.test.ts index 0a817e2859..638c4c3fd7 100644 --- a/tests/unit/lib/managementCliToken.test.ts +++ b/tests/unit/lib/managementCliToken.test.ts @@ -20,9 +20,9 @@ await settingsDb.updateSettings({ password: "test-password-hash", }); -const { getLegacyCliTokenSync, getMachineTokenSync } = await import( - "../../../src/lib/machineToken.ts" -); +const { getLegacyCliTokenSync, getMachineTokenSync } = + await import("../../../src/lib/machineToken.ts"); +const { requireManagementAuth } = await import("../../../src/lib/api/requireManagementAuth.ts"); const { managementPolicy } = await import("../../../src/server/authz/policies/management.ts"); const { CLI_TOKEN_HEADER } = await import("../../../src/server/authz/headers.ts"); @@ -103,3 +103,34 @@ test("management policy rejects wrong CLI token from localhost", async () => { const outcome = await managementPolicy.evaluate(ctx); assert.equal(outcome.allow, false); }); + +test("route-level auth trusts only the central local-CLI subject stamp", async () => { + const request = new Request("http://localhost/api/cli/whoami", { + headers: { + "x-omniroute-auth-kind": "management_key", + "x-omniroute-auth-label": "local-cli-token", + }, + }); + assert.equal(await requireManagementAuth(request, { alwaysRequireAuth: true }), null); + + const spoofedLabelOnly = new Request("http://localhost/api/cli/whoami", { + headers: { "x-omniroute-auth-label": "local-cli-token" }, + }); + assert.notEqual(await requireManagementAuth(spoofedLabelOnly, { alwaysRequireAuth: true }), null); +}); + +test("management policy rejects machine tokens when CLI-token auth is disabled", async () => { + const previous = process.env.OMNIROUTE_DISABLE_CLI_TOKEN; + process.env.OMNIROUTE_DISABLE_CLI_TOKEN = "true"; + try { + const ctx = makeCtx( + { host: "localhost", [CLI_TOKEN_HEADER]: getMachineTokenSync() }, + { socket: { remoteAddress: "127.0.0.1" } } + ); + const outcome = await managementPolicy.evaluate(ctx); + assert.equal(outcome.allow, false); + } finally { + if (previous === undefined) delete process.env.OMNIROUTE_DISABLE_CLI_TOKEN; + else process.env.OMNIROUTE_DISABLE_CLI_TOKEN = previous; + } +}); diff --git a/tests/unit/next-config.test.ts b/tests/unit/next-config.test.ts index 9281175217..6291fb69f2 100644 --- a/tests/unit/next-config.test.ts +++ b/tests/unit/next-config.test.ts @@ -105,6 +105,7 @@ test("next config declares Turbopack aliases, runtime assets and server external // sqlite-vec ships a native vec0.so loaded at runtime; without externalizing it // the Turbopack build fails with "Unknown module type" on the .so (issue #3066). "sqlite-vec", + "node-machine-id", "wreq-js", "fs", "path", @@ -126,10 +127,7 @@ test("Turbopack aliases @/mitm/manager to the stub ONLY when OMNIROUTE_MITM_STUB process.env.OMNIROUTE_MITM_STUB = "1"; const { default: docker } = await loadNextConfig("mitm-docker"); - assert.equal( - docker.turbopack.resolveAlias["@/mitm/manager"], - "./src/mitm/manager.stub.ts" - ); + assert.equal(docker.turbopack.resolveAlias["@/mitm/manager"], "./src/mitm/manager.stub.ts"); } finally { if (original === undefined) delete process.env.OMNIROUTE_MITM_STUB; else process.env.OMNIROUTE_MITM_STUB = original; @@ -198,7 +196,11 @@ test("manager.stub.ts exports every name statically imported from @/mitm/manager } for (const m of stubSrc.matchAll(/export\s*\{([^}]*)\}/g)) { for (const part of m[1].split(",")) { - const exported = part.trim().split(/\s+as\s+/).pop()?.trim(); // `x as y` exports y + const exported = part + .trim() + .split(/\s+as\s+/) + .pop() + ?.trim(); // `x as y` exports y if (exported) stubExports.add(exported); } } From a72dc25c04d948b40f37d7a742623b4492f188f5 Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:19:32 +0700 Subject: [PATCH 088/135] fix(proxy): stop reporting IPv4-only proxies as dead (#10868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug real e bem raiz-causado: api64.ipify.org é IPv6-first e derruba tunnels IPv4-only, o que estava reportando proxies vivos como mortos. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity 2563/2774, cognitive-complexity 1155/1223 (baseline) - tests/unit/proxy-echo-ipv4-fallback-9694.test.ts — 8/8 passando (cobre ordem, split de budget, override, proxy morto de verdade) - Suítes proxy-relacionadas: 805/817 na branch vs 797/809 no release, as 11 falhas são idênticas em ambos os lados e não relacionadas (TLS transport, tproxy CA, SSRF fallback) --- .../fixes/10868-proxy-echo-ipv4-fallback.md | 1 + docs/reference/ENVIRONMENT.md | 1 + .../free-proxies/[id]/add-to-pool/route.ts | 35 +++-- .../free-proxies/bulk-add-to-pool/route.ts | 34 +++-- src/app/api/settings/proxy/test/route.ts | 33 +++-- src/lib/proxyEchoTarget.ts | 90 ++++++++++++ src/lib/proxyEgress.ts | 65 ++++++--- .../proxy-echo-ipv4-fallback-9694.test.ts | 138 ++++++++++++++++++ 8 files changed, 336 insertions(+), 61 deletions(-) create mode 100644 changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md create mode 100644 src/lib/proxyEchoTarget.ts create mode 100644 tests/unit/proxy-echo-ipv4-fallback-9694.test.ts diff --git a/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md b/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md new file mode 100644 index 0000000000..91f4e717e8 --- /dev/null +++ b/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md @@ -0,0 +1 @@ +- **fix(proxy):** proxy "Test connection" no longer reports an IPv4-only SOCKS5/SSH proxy as dead. #1255 moved every egress probe from `api.ipify.org` to `api64.ipify.org` so proxies with IPv6 egress could be tested, but `api64` is IPv6-first: a tunnel with no IPv6 route has nothing to connect to, so the probe hung until the caller's deadline and a proxy that was carrying live LLM traffic came back as a failure. Swapping the target to `api4` fixes that case and re-breaks the one #1255 fixed, so the probe now tries the targets in order instead — `api64` first, so a proxy with working IPv6 answers on the first attempt and keeps the exact behaviour #1255 introduced, including which of its addresses is reported (the egress IP is used as an identity to detect accounts of one rotation group sharing an address, so the attempts are sequential rather than raced). The attempts split the budget each call site already enforced, so no probe can take longer than it could before, and each attempt gets its own `AbortController` so exhausting the budget on an unreachable target does not abort the next one. `OMNIROUTE_PROXY_ECHO_URL` pins a single target — including a self-hosted echo — replacing the workaround of rewriting the compiled bundle after every upgrade. The relay branch of the test route still targets `api64` through `x-relay-target`, since that request egresses from the relay worker rather than the operator's tunnel diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 73a0c8a08d..bf3fbd68d2 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -349,6 +349,7 @@ Route upstream LLM provider calls through an HTTP or SOCKS5 proxy for egress con | `HTTP_PROXY` | _(unset)_ | Node.js standard | HTTP proxy for upstream calls. | | `HTTPS_PROXY` | _(unset)_ | Node.js standard | HTTPS proxy for upstream calls. | | `ALL_PROXY` | _(unset)_ | Node.js standard | Universal proxy (supports `socks5://`). | +| `OMNIROUTE_PROXY_ECHO_URL` | _(unset)_ | `src/lib/proxyEchoTarget.ts` | Pins the echo-IP target used by proxy egress probes to a single URL. Unset, the probe tries `api64.ipify.org` then `api4.ipify.org` so IPv4-only tunnels are not reported dead (#9694). | | `NO_PROXY` | _(unset)_ | Node.js standard | Comma-separated hostnames/IPs to bypass the proxy. | | `OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS` | `32` | `open-sse/utils/proxyDispatcher.ts` | Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher. Long-lived SSE streams such as Codex `/v1/responses` need more than one connection when several requests share the same account-level proxy. Values above `256` are capped. | | `SOCKS_HANDSHAKE_TIMEOUT_MS` | `10000` | `open-sse/utils/socksConnectorWithFamily.ts` | SOCKS5 handshake (connect) timeout in ms. Raise it when a single residential gateway host is hit by high concurrency (e.g. 100 simultaneous requests) — the real handshake can exceed 10s under a saturated pool even though the proxy is reachable, which otherwise surfaces as a false `[Proxy Fast-Fail] Proxy unreachable`. Capped at `120000`. | diff --git a/src/app/api/settings/free-proxies/[id]/add-to-pool/route.ts b/src/app/api/settings/free-proxies/[id]/add-to-pool/route.ts index 2cfb6ddefa..2e13845746 100644 --- a/src/app/api/settings/free-proxies/[id]/add-to-pool/route.ts +++ b/src/app/api/settings/free-proxies/[id]/add-to-pool/route.ts @@ -6,6 +6,7 @@ import { createProxyDispatcher, proxyConfigToUrl, } from "@omniroute/open-sse/utils/proxyDispatcher.ts"; +import { probeEchoTargets } from "@/lib/proxyEchoTarget"; type ConnectivityTester = ( host: string, @@ -23,31 +24,37 @@ async function testProxyConnectivity( const dispatcher = createProxyDispatcher(proxyUrl); const start = Date.now(); - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); try { - const res = await undiciRequest("https://api64.ipify.org?format=json", { - method: "GET", - dispatcher, - signal: controller.signal, - headersTimeout: 5000, - bodyTimeout: 5000, - }); - const text = await res.body.text(); + // #9694: try the IPv6-first echo target, then the IPv4-only one, so a proxy + // with no IPv6 route is not reported dead. + const { result } = await probeEchoTargets(async (url, timeoutMs) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await undiciRequest(url, { + method: "GET", + dispatcher, + signal: controller.signal, + headersTimeout: timeoutMs, + bodyTimeout: timeoutMs, + }); + return { statusCode: res.statusCode, text: await res.body.text() }; + } finally { + clearTimeout(timeout); + } + }, 5000); let parsed: { ip?: string } = {}; try { - parsed = JSON.parse(text) as { ip?: string }; + parsed = JSON.parse(result.text) as { ip?: string }; } catch {} return { - success: res.statusCode === 200, + success: result.statusCode === 200, latencyMs: Date.now() - start, publicIp: parsed.ip, }; } catch { return { success: false, latencyMs: Date.now() - start }; - } finally { - clearTimeout(timeout); } } diff --git a/src/app/api/settings/free-proxies/bulk-add-to-pool/route.ts b/src/app/api/settings/free-proxies/bulk-add-to-pool/route.ts index 20d9f2c53c..c7d2108d5e 100644 --- a/src/app/api/settings/free-proxies/bulk-add-to-pool/route.ts +++ b/src/app/api/settings/free-proxies/bulk-add-to-pool/route.ts @@ -8,6 +8,7 @@ import { createProxyDispatcher, proxyConfigToUrl, } from "@omniroute/open-sse/utils/proxyDispatcher.ts"; +import { probeEchoTargets } from "@/lib/proxyEchoTarget"; type QuickTester = ( host: string, @@ -24,22 +25,29 @@ async function testProxyQuick( if (!proxyUrl) return { ok: false, latencyMs: 0 }; const dispatcher = createProxyDispatcher(proxyUrl); const start = Date.now(); - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); try { - const res = await undiciRequest("https://api64.ipify.org?format=json", { - method: "GET", - dispatcher, - signal: controller.signal, - headersTimeout: 5000, - bodyTimeout: 5000, - }); - await res.body.dump(); - return { ok: res.statusCode === 200, latencyMs: Date.now() - start }; + // #9694: try the IPv6-first echo target, then the IPv4-only one, so a proxy + // with no IPv6 route is not reported dead. + const { result } = await probeEchoTargets(async (url, timeoutMs) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await undiciRequest(url, { + method: "GET", + dispatcher, + signal: controller.signal, + headersTimeout: timeoutMs, + bodyTimeout: timeoutMs, + }); + await res.body.dump(); + return res.statusCode; + } finally { + clearTimeout(timeout); + } + }, 5000); + return { ok: result === 200, latencyMs: Date.now() - start }; } catch { return { ok: false, latencyMs: Date.now() - start }; - } finally { - clearTimeout(timeout); } } diff --git a/src/app/api/settings/proxy/test/route.ts b/src/app/api/settings/proxy/test/route.ts index c1eaf085b9..c2ceef616b 100644 --- a/src/app/api/settings/proxy/test/route.ts +++ b/src/app/api/settings/proxy/test/route.ts @@ -6,6 +6,7 @@ import { proxyConfigToUrl, proxyUrlForLogs, } from "@omniroute/open-sse/utils/proxyDispatcher.ts"; +import { probeEchoTargets } from "@/lib/proxyEchoTarget"; import { testProxySchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; @@ -215,20 +216,28 @@ export async function POST(request: Request) { const publicProxyUrl = proxyUrlForLogs(proxyUrl); const startTime = Date.now(); - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 10000); const dispatcher = createProxyDispatcher(proxyUrl); try { - const result = await undiciRequest("https://api64.ipify.org?format=json", { - method: "GET", - dispatcher, - signal: controller.signal, - headersTimeout: 10000, - bodyTimeout: 10000, - }); - - const responseText = await result.body.text(); + // #9694: an IPv4-only SOCKS5/SSH tunnel has no route to the IPv6-first + // echo target and used to hang here until the deadline, reporting a + // healthy proxy as dead. Each target gets its own slice of the budget. + const { result: responseText } = await probeEchoTargets(async (url, timeoutMs) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const result = await undiciRequest(url, { + method: "GET", + dispatcher, + signal: controller.signal, + headersTimeout: timeoutMs, + bodyTimeout: timeoutMs, + }); + return await result.body.text(); + } finally { + clearTimeout(timeout); + } + }, 10000); let parsed: { ip?: string }; try { const parsedJson = JSON.parse(responseText); @@ -260,8 +269,6 @@ export async function POST(request: Request) { latencyMs: Date.now() - startTime, proxyUrl: publicProxyUrl, }); - } finally { - clearTimeout(timeout); } } catch (error) { return createErrorResponseFromUnknown(error, "Unexpected server error"); diff --git a/src/lib/proxyEchoTarget.ts b/src/lib/proxyEchoTarget.ts new file mode 100644 index 0000000000..84cd9ef35c --- /dev/null +++ b/src/lib/proxyEchoTarget.ts @@ -0,0 +1,90 @@ +/** + * #9694 — echo-IP target selection for proxy egress probes. + * + * #1255 moved every probe from `api.ipify.org` to `api64.ipify.org` so proxies + * with IPv6 egress could be tested. `api64` is IPv6-first, so it broke the case + * the other way: an IPv4-only SOCKS5/SSH tunnel has no route to it and the probe + * hangs until the caller's deadline, reporting a healthy proxy as dead. + * + * Neither single target works for both, so the probe tries them in order and + * splits the caller's existing budget between the attempts. `api64` stays first, + * so a proxy with working IPv6 answers on the first attempt and keeps the exact + * behaviour #1255 introduced — including which of its addresses is reported, + * which matters because the egress IP is an identity used to detect accounts + * sharing an address. Only a proxy that cannot reach `api64` at all pays for the + * second attempt, and the total stays bounded by the budget the caller already + * enforced. + * + * Dependency-free leaf so the ordering and budget arithmetic are unit-testable + * without opening a socket. + */ + +/** IPv6-first echo target (#1255). Answers over IPv4 too when IPv6 is unavailable to the resolver. */ +export const EGRESS_ECHO_URL_DUAL = "https://api64.ipify.org?format=json"; + +/** IPv4-only echo target — reachable from a proxy with no IPv6 route. */ +export const EGRESS_ECHO_URL_V4 = "https://api4.ipify.org?format=json"; + +/** Operators can pin a single target (including a self-hosted echo) per deployment. */ +export const EGRESS_ECHO_URL_ENV = "OMNIROUTE_PROXY_ECHO_URL"; + +/** Minimum a single attempt may be given, so a small caller budget is not split into uselessly short tries. */ +export const MIN_ECHO_ATTEMPT_MS = 2000; + +/** + * Ordered echo targets. An override pins exactly one target — an operator who + * names a target means it, and silently trying ipify anyway would defeat the + * point of pointing the probe at a self-hosted echo. + */ +export function resolveEgressEchoUrls( + env: Record = process.env +): string[] { + const override = env[EGRESS_ECHO_URL_ENV]; + if (typeof override === "string" && override.trim().length > 0) return [override.trim()]; + return [EGRESS_ECHO_URL_DUAL, EGRESS_ECHO_URL_V4]; +} + +/** + * Per-attempt budget. The attempts must fit inside the budget the caller already + * enforces, so the deadline the operator sees does not move. A budget too small + * to split fairly is spent entirely on the first target rather than on two + * attempts that are each too short to succeed. + */ +export function splitEchoAttemptBudget(totalMs: number, attempts: number): number[] { + if (!Number.isFinite(totalMs) || totalMs <= 0 || attempts <= 0) return []; + if (attempts === 1) return [totalMs]; + const even = Math.floor(totalMs / attempts); + if (even < MIN_ECHO_ATTEMPT_MS) return [totalMs]; + return Array.from({ length: attempts }, () => even); +} + +export interface EchoAttemptOutcome { + result: T; + url: string; +} + +/** + * Try each echo target in order until one resolves. Rethrows the LAST error when + * every target fails, so the caller's error message still describes a real + * network failure rather than a bookkeeping one. + */ +export async function probeEchoTargets( + run: (url: string, timeoutMs: number) => Promise, + totalMs: number, + env?: Record +): Promise> { + const urls = resolveEgressEchoUrls(env); + const budgets = splitEchoAttemptBudget(totalMs, urls.length); + const attempts = budgets.length; + let lastError: unknown = new Error("no echo target attempted"); + + for (let i = 0; i < attempts; i++) { + const url = urls[i]; + try { + return { result: await run(url, budgets[i]), url }; + } catch (error) { + lastError = error; + } + } + throw lastError; +} diff --git a/src/lib/proxyEgress.ts b/src/lib/proxyEgress.ts index 6f8b10e81a..bcad4e2d7b 100644 --- a/src/lib/proxyEgress.ts +++ b/src/lib/proxyEgress.ts @@ -13,10 +13,13 @@ * entering and leaving by. */ import { request as undiciRequest } from "undici"; -import { createProxyDispatcher, proxyConfigToUrl } from "@omniroute/open-sse/utils/proxyDispatcher.ts"; +import { + createProxyDispatcher, + proxyConfigToUrl, +} from "@omniroute/open-sse/utils/proxyDispatcher.ts"; import { rotationGroupFor } from "@omniroute/open-sse/services/refreshSerializer.ts"; +import { probeEchoTargets } from "./proxyEchoTarget"; -const EGRESS_ECHO_URL = "https://api64.ipify.org?format=json"; const EGRESS_PROBE_TIMEOUT_MS = 6000; const EGRESS_CACHE_TTL_MS = 5 * 60 * 1000; @@ -32,18 +35,26 @@ const egressCache = new Map(); async function defaultEgressProbe(proxyUrl: string | null): Promise { const start = Date.now(); - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), EGRESS_PROBE_TIMEOUT_MS); try { const dispatcher = proxyUrl ? createProxyDispatcher(proxyUrl) : undefined; - const res = await undiciRequest(EGRESS_ECHO_URL, { - method: "GET", - dispatcher, - signal: controller.signal, - headersTimeout: EGRESS_PROBE_TIMEOUT_MS, - bodyTimeout: EGRESS_PROBE_TIMEOUT_MS, - }); - const text = await res.body.text(); + // #9694: each echo target gets its own controller, so exhausting the budget + // on an unreachable IPv6-first target does not abort the IPv4 attempt. + const { result: text } = await probeEchoTargets(async (url, timeoutMs) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await undiciRequest(url, { + method: "GET", + dispatcher, + signal: controller.signal, + headersTimeout: timeoutMs, + bodyTimeout: timeoutMs, + }); + return await res.body.text(); + } finally { + clearTimeout(timeout); + } + }, EGRESS_PROBE_TIMEOUT_MS); let ip: string | null = null; try { ip = (JSON.parse(text) as { ip?: string }).ip ?? null; @@ -57,8 +68,6 @@ async function defaultEgressProbe(proxyUrl: string | null): Promise(); @@ -195,9 +204,7 @@ export async function diagnoseAllEgressIps(deps?: { getConnections?: () => Promise< Array<{ id: string; provider: string; name?: string; email?: string; authType?: string }> >; - resolveProxy?: ( - connectionId: string - ) => Promise<{ proxy?: unknown; level?: string } | null>; + resolveProxy?: (connectionId: string) => Promise<{ proxy?: unknown; level?: string } | null>; }): Promise { const getConnections = deps?.getConnections ?? @@ -266,9 +273,21 @@ export interface ProxyValidationResult { */ export async function validateProxyPool(deps?: { listProxies?: () => Promise< - Array<{ id: string; type: string; host: string; port: number | string; username?: string | null; password?: string | null; status?: string | null }> + Array<{ + id: string; + type: string; + host: string; + port: number | string; + username?: string | null; + password?: string | null; + status?: string | null; + }> >; - markStatus?: (id: string, status: string, meta: { latencyMs: number; egressIp: string | null }) => Promise; + markStatus?: ( + id: string, + status: string, + meta: { latencyMs: number; egressIp: string | null } + ) => Promise; }): Promise { const listProxies = deps?.listProxies ?? @@ -351,7 +370,11 @@ export function planProxyDistribution( return; } if (opts.allowSharing) { - assignments.push({ connectionId: c.id, account, proxyId: liveProxyIds[i % liveProxyIds.length] }); + assignments.push({ + connectionId: c.id, + account, + proxyId: liveProxyIds[i % liveProxyIds.length], + }); } else if (i < liveProxyIds.length) { assignments.push({ connectionId: c.id, account, proxyId: liveProxyIds[i] }); } else { diff --git a/tests/unit/proxy-echo-ipv4-fallback-9694.test.ts b/tests/unit/proxy-echo-ipv4-fallback-9694.test.ts new file mode 100644 index 0000000000..ac19c21ff5 --- /dev/null +++ b/tests/unit/proxy-echo-ipv4-fallback-9694.test.ts @@ -0,0 +1,138 @@ +/** + * #9694 — proxy "Test connection" false-negative on IPv4-only SOCKS5/SSH proxies. + * + * #1255 moved every egress probe to `api64.ipify.org`, which is IPv6-first, so a + * proxy with no IPv6 route has nothing to connect to and the probe hangs until the + * caller's deadline — a healthy proxy reported dead. Swapping the target to + * `api4.ipify.org` fixes that case and breaks #1255's. + * + * The probe now tries the targets in order inside the budget the caller already + * enforced. `api64` stays FIRST so a proxy with working IPv6 behaves exactly as it + * did after #1255 — including which of its addresses is reported, which matters + * because the egress IP is used as an identity to detect accounts sharing an address. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { + probeEchoTargets, + resolveEgressEchoUrls, + splitEchoAttemptBudget, + EGRESS_ECHO_URL_DUAL, + EGRESS_ECHO_URL_V4, + EGRESS_ECHO_URL_ENV, + MIN_ECHO_ATTEMPT_MS, +} = await import("../../src/lib/proxyEchoTarget.ts"); + +test("#9694: the IPv6-first target is still tried first", () => { + assert.deepEqual(resolveEgressEchoUrls({}), [EGRESS_ECHO_URL_DUAL, EGRESS_ECHO_URL_V4]); + assert.equal(EGRESS_ECHO_URL_DUAL, "https://api64.ipify.org?format=json"); + assert.equal(EGRESS_ECHO_URL_V4, "https://api4.ipify.org?format=json"); +}); + +test("#9694: an operator override pins exactly one target", () => { + const env = { [EGRESS_ECHO_URL_ENV]: " https://echo.internal/ip " }; + assert.deepEqual(resolveEgressEchoUrls(env), ["https://echo.internal/ip"]); + for (const blank of ["", " "]) { + assert.deepEqual(resolveEgressEchoUrls({ [EGRESS_ECHO_URL_ENV]: blank }), [ + EGRESS_ECHO_URL_DUAL, + EGRESS_ECHO_URL_V4, + ]); + } +}); + +test("#9694: attempts share the caller's budget instead of extending it", () => { + // The real call sites use 5s, 6s and 10s. + assert.deepEqual(splitEchoAttemptBudget(10000, 2), [5000, 5000]); + assert.deepEqual(splitEchoAttemptBudget(6000, 2), [3000, 3000]); + assert.deepEqual(splitEchoAttemptBudget(5000, 2), [2500, 2500]); + for (const total of [10000, 6000, 5000]) { + const budgets = splitEchoAttemptBudget(total, 2); + assert.ok( + budgets.reduce((a, b) => a + b, 0) <= total, + "the sum must never exceed the deadline the caller already enforced" + ); + } +}); + +test("#9694: a budget too small to split is spent on one attempt, not two useless ones", () => { + assert.deepEqual(splitEchoAttemptBudget(MIN_ECHO_ATTEMPT_MS * 2 - 2, 2), [ + MIN_ECHO_ATTEMPT_MS * 2 - 2, + ]); + assert.deepEqual(splitEchoAttemptBudget(0, 2), []); + assert.deepEqual(splitEchoAttemptBudget(-1, 2), []); + assert.deepEqual(splitEchoAttemptBudget(10000, 0), []); + assert.deepEqual(splitEchoAttemptBudget(10000, 1), [10000]); +}); + +test("#9694: a reachable IPv6-first target is used and the IPv4 target is never touched", async () => { + const tried: string[] = []; + const outcome = await probeEchoTargets( + async (url) => { + tried.push(url); + return '{"ip":"2001:db8::1"}'; + }, + 10000, + {} + ); + assert.deepEqual(tried, [EGRESS_ECHO_URL_DUAL], "no extra request for a healthy IPv6 proxy"); + assert.equal(outcome.url, EGRESS_ECHO_URL_DUAL); + assert.equal(outcome.result, '{"ip":"2001:db8::1"}'); +}); + +test("#9694: an IPv4-only proxy reaches the IPv4 target and succeeds", async () => { + const tried: Array<{ url: string; timeoutMs: number }> = []; + const outcome = await probeEchoTargets( + async (url, timeoutMs) => { + tried.push({ url, timeoutMs }); + // What an IPv4-only SOCKS5 tunnel does with an IPv6-first host: nothing, + // until the attempt budget aborts it. + if (url === EGRESS_ECHO_URL_DUAL) throw new Error("This operation was aborted"); + return '{"ip":"203.0.113.7"}'; + }, + 10000, + {} + ); + assert.deepEqual( + tried.map((t) => t.url), + [EGRESS_ECHO_URL_DUAL, EGRESS_ECHO_URL_V4], + "the IPv6 attempt must not end the probe" + ); + assert.deepEqual( + tried.map((t) => t.timeoutMs), + [5000, 5000], + "each attempt gets half of the caller's 10s budget" + ); + assert.equal(outcome.url, EGRESS_ECHO_URL_V4); + assert.equal(outcome.result, '{"ip":"203.0.113.7"}'); +}); + +test("#9694: a genuinely dead proxy still fails, with the last real error", async () => { + await assert.rejects( + () => + probeEchoTargets( + async (url) => { + throw new Error(`ECONNREFUSED ${url}`); + }, + 10000, + {} + ), + /ECONNREFUSED .*api4\.ipify\.org/, + "the surfaced error must describe a network failure, not internal bookkeeping" + ); +}); + +test("#9694: an override that fails is not silently retried against ipify", async () => { + const tried: string[] = []; + await assert.rejects(() => + probeEchoTargets( + async (url) => { + tried.push(url); + throw new Error("nope"); + }, + 10000, + { [EGRESS_ECHO_URL_ENV]: "https://echo.internal/ip" } + ) + ); + assert.deepEqual(tried, ["https://echo.internal/ip"]); +}); From 80b517edeaaf778daa44fb6213f369e2948e5a4e Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:19:41 +0700 Subject: [PATCH 089/135] fix(providers): treat a degraded cached catalog as a failed model sync (#10862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — root cause preciso: a rota sync-models só reconhecia a degradação para local_catalog, não para o fallback de cache com warning, então uma chave expirada (401) virava silenciosamente "Nenhum modelo novo foi adicionado" em vez de um erro visível. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/sync-models-degraded-cached-catalog-9683.test.ts — 6/6 passando (payloads reais do models/route.ts) - Suítes model-sync/provider-models/sync-models/siliconflow — 181/181 passando, incluindo as 3 asserções pré-existentes #5460/#5465 --- ...862-sync-models-degraded-cached-catalog.md | 1 + .../[id]/sync-models/degradedLocalCatalog.ts | 41 ++++++ .../api/providers/[id]/sync-models/route.ts | 6 +- ...odels-degraded-cached-catalog-9683.test.ts | 121 ++++++++++++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md create mode 100644 tests/unit/sync-models-degraded-cached-catalog-9683.test.ts diff --git a/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md b/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md new file mode 100644 index 0000000000..fd6f7d3f04 --- /dev/null +++ b/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md @@ -0,0 +1 @@ +- **fix(providers):** importing models with an expired API key now surfaces the credential error instead of reporting "No new models were added". The Import button posts to `/api/providers/{id}/sync-models`, which self-fetches the models route; that route does not fail on an upstream 401 but degrades to a catalog it already has, preferring the cache and using the local catalog only when there is no cache. A provider that imported successfully once therefore has a cache, so an expired key produced `{ source: "cache", warning: "Models probe failed (401) — using cached catalog" }` with HTTP 200 — and the #5460/#5465 degradation guard only recognised the `local_catalog` branch, so model-sync accepted it as a successful discovery, found every cached model already imported, and returned the empty-diff result. Retest does not go through this path, which is why it failed correctly and made the import look like a genuine "nothing to do". The existing rule — a degraded discovery must not be persisted as the synced catalog — is now applied to the branch it missed rather than special-casing 401/403, discriminating on the warning the fallback builder always attaches (an ordinary non-refresh cache hit attaches none, and model-sync always requests `refresh=true`). `isDegradedLocalCatalog` keeps its exact meaning and its existing tests diff --git a/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts b/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts index 6e521e81ce..dc2d8966aa 100644 --- a/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts +++ b/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts @@ -19,3 +19,44 @@ export function isDegradedLocalCatalog(modelsData: { typeof modelsData?.source === "string" ? modelsData.source.trim().toLowerCase() : ""; return source === "local_catalog" && modelsData?.intentional !== true; } + +/** + * #9683 — the same degradation, one branch further up. + * + * When remote discovery fails, the models route falls back to the CACHED + * catalog if it has one and only falls back to the local catalog when it does + * not (`buildDiscoveryFallbackResponse`). A provider that was imported + * successfully once therefore has a cache, so an expired key produced + * `source: "cache"` + a warning and HTTP 200 — model-sync treated that as a + * successful discovery, found every cached model already imported, and reported + * "No new models were added" instead of the credential error. Retest, which + * does not go through this path, failed correctly, which is what made the + * import look like a real "nothing to do". + * + * The discriminator is the warning: the fallback builder always attaches one, + * while the ordinary cache hit (`maybeReturnCachedDiscovery`, a non-refresh + * read) attaches none. Model-sync always requests `refresh=true`, so the one + * warning-carrying cache response it can observe is a degraded one. + */ +export function isDegradedCachedCatalog(modelsData: { + source?: unknown; + warning?: unknown; +}): boolean { + const source = + typeof modelsData?.source === "string" ? modelsData.source.trim().toLowerCase() : ""; + if (source !== "cache") return false; + return typeof modelsData?.warning === "string" && modelsData.warning.trim().length > 0; +} + +/** + * Either degraded shape. Model-sync must refuse to treat these as a successful + * discovery: persisting them would silently pin a stale catalog and hide the + * real failure from the operator. + */ +export function isDegradedDiscovery(modelsData: { + source?: unknown; + intentional?: unknown; + warning?: unknown; +}): boolean { + return isDegradedLocalCatalog(modelsData) || isDegradedCachedCatalog(modelsData); +} diff --git a/src/app/api/providers/[id]/sync-models/route.ts b/src/app/api/providers/[id]/sync-models/route.ts index 95986e15a9..1d46c269f0 100644 --- a/src/app/api/providers/[id]/sync-models/route.ts +++ b/src/app/api/providers/[id]/sync-models/route.ts @@ -22,7 +22,7 @@ import { autoSyncCodexProfilesFromLiveCatalog } from "@/lib/cli-helper/codexProf import { autoSyncClaudeProfilesFromLiveCatalog } from "@/lib/cli-helper/claudeProfileAutoSync"; import { providerUsesCuratedModelsOnly } from "@/lib/providers/modelListingCapability"; import { GET as getProviderModels } from "../models/route"; -import { isDegradedLocalCatalog } from "./degradedLocalCatalog"; +import { isDegradedDiscovery } from "./degradedLocalCatalog"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; type JsonRecord = Record; @@ -465,9 +465,9 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: const modelSource = toNonEmptyString(modelsData.source)?.toLowerCase() || "unknown"; const modelWarning = toNonEmptyString(modelsData.warning); - if (isDegradedLocalCatalog(modelsData)) { + if (isDegradedDiscovery(modelsData)) { const responseError = - modelWarning || "Remote model discovery failed; local catalog fallback not synced"; + modelWarning || "Remote model discovery failed; catalog fallback not synced"; await saveCallLog({ method: "GET", path: `/api/providers/${id}/models`, diff --git a/tests/unit/sync-models-degraded-cached-catalog-9683.test.ts b/tests/unit/sync-models-degraded-cached-catalog-9683.test.ts new file mode 100644 index 0000000000..beb0fe76c0 --- /dev/null +++ b/tests/unit/sync-models-degraded-cached-catalog-9683.test.ts @@ -0,0 +1,121 @@ +/** + * #9683 — "Importing Models" reported success with an expired API key. + * + * The import button posts to `/api/providers/{id}/sync-models`, which self-fetches + * `/api/providers/{id}/models?refresh=true`. On an upstream 401 that route does not + * fail: it falls back to a catalog it already has. It prefers the CACHE and only uses + * the local catalog when there is no cache, so a provider that imported successfully + * once takes the cache branch — `{ source: "cache", warning: "…(401)…" }` with HTTP + * 200. `isDegradedLocalCatalog` only recognised `local_catalog`, so model-sync treated + * that as a successful discovery, found every cached model already imported and + * answered "No new models were added" instead of surfacing the credential error. + * + * The warning is the discriminator: `buildDiscoveryFallbackResponse` always attaches + * one, while the ordinary non-refresh cache hit attaches none. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { isDegradedLocalCatalog, isDegradedCachedCatalog, isDegradedDiscovery } = + await import("../../src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts"); + +// The exact payload shapes `src/app/api/providers/[id]/models/route.ts` emits from +// `buildCachedDiscoveryResponse(cacheWarning)` on a failed probe. +const AUTH_FAILED_CACHE = { + source: "cache", + warning: "Models probe failed (401) — using cached catalog", +}; +const BEDROCK_AUTH_FAILED_CACHE = { + source: "cache", + warning: "Auth failed (403) — using cached catalog", +}; +const UNAVAILABLE_CACHE = { + source: "cache", + warning: "API unavailable — using cached catalog", +}; +// `maybeReturnCachedDiscovery()` — an ordinary cache hit, built with no warning. +const HEALTHY_CACHE = { source: "cache" }; + +test("#9683: a 401 that degraded to the cached catalog is a failed discovery", () => { + assert.equal( + isDegradedCachedCatalog(AUTH_FAILED_CACHE), + true, + "an expired key must not be reported to the operator as a successful import" + ); + assert.equal(isDegradedCachedCatalog(BEDROCK_AUTH_FAILED_CACHE), true); + assert.equal(isDegradedDiscovery(AUTH_FAILED_CACHE), true); +}); + +test("#9683: any warning-carrying cache fallback is degraded, not only auth", () => { + // The rule model-sync already applies to `local_catalog` is not auth-specific: + // a degraded discovery must not be persisted as the synced catalog. + assert.equal(isDegradedCachedCatalog(UNAVAILABLE_CACHE), true); + assert.equal(isDegradedDiscovery(UNAVAILABLE_CACHE), true); +}); + +test("#9683: an ordinary cache hit stays a success", () => { + assert.equal(isDegradedCachedCatalog(HEALTHY_CACHE), false); + assert.equal(isDegradedDiscovery(HEALTHY_CACHE), false); + for (const blank of ["", " "]) { + assert.equal( + isDegradedCachedCatalog({ source: "cache", warning: blank }), + false, + "a blank warning is not a degradation signal" + ); + } + assert.equal(isDegradedCachedCatalog({ source: "cache", warning: 42 }), false); +}); + +test("#9683: only the cache source is judged by this predicate", () => { + assert.equal(isDegradedCachedCatalog({ source: "api", warning: "anything" }), false); + assert.equal(isDegradedCachedCatalog({ source: "local_catalog", warning: "x" }), false); + assert.equal(isDegradedCachedCatalog({}), false); + assert.equal(isDegradedCachedCatalog({ source: "" }), false); + assert.equal(isDegradedCachedCatalog({ source: " CACHE ", warning: "x" }), true); +}); + +// ── #5460/#5465 must be unchanged ───────────────────────────────────────── + +test("#9683: the local-catalog rule is untouched", () => { + assert.equal(isDegradedLocalCatalog({ source: "local_catalog", intentional: true }), false); + assert.equal(isDegradedLocalCatalog({ source: "local_catalog", intentional: false }), true); + assert.equal(isDegradedLocalCatalog({ source: "cache", intentional: false }), false); + + assert.equal( + isDegradedDiscovery({ source: "local_catalog", intentional: true }), + false, + "a provider whose local catalog is its only discovery source still syncs" + ); + assert.equal( + isDegradedDiscovery({ + source: "local_catalog", + intentional: true, + warning: "reka has no remote /models endpoint", + }), + false, + "an intentional local catalog is not degraded just because it carries a warning" + ); + assert.equal(isDegradedDiscovery({ source: "local_catalog", intentional: false }), true); +}); + +// ── Wiring ──────────────────────────────────────────────────────────────── +// The predicate is only half the fix: the route has to consult the combined one. +// This assertion fails on the pre-fix tree, where the guard read +// `isDegradedLocalCatalog(modelsData)` and so never saw the cache fallback. + +test("#9683: the sync-models route gates on the combined predicate", async () => { + const { readFileSync } = await import("node:fs"); + const source = readFileSync( + new URL("../../src/app/api/providers/[id]/sync-models/route.ts", import.meta.url), + "utf8" + ); + assert.match( + source, + /if\s*\(\s*isDegradedDiscovery\(modelsData\)\s*\)/, + "model-sync must refuse both degraded shapes, not just local_catalog" + ); + assert.ok( + !/isDegradedLocalCatalog\(modelsData\)/.test(source), + "the narrow predicate must no longer be the route's only gate" + ); +}); From d99701d6b3c6f76c2f22e517967128e28af6e81f Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:19:50 +0700 Subject: [PATCH 090/135] fix(mcp): give provider-bound tool calls their own fetch budget (#10860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — o hop de routing (route_request) herdava o budget de 10s de management em vez do budget de 60s de upstream que web_search/web_fetch já usavam, então uma rota de 35-40s abortava só pelo lado do MCP. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/mcp-upstream-fetch-timeout-9717.test.ts — 6/6 passando - Suíte MCP completa — 149/153 (branch) vs 143/147 (release), as 4 falhas são idênticas nos dois lados e não relacionadas (closure de package-files, resolução de bundle dist/) --- .../fixes/10860-mcp-upstream-fetch-timeout.md | 1 + docs/frameworks/MCP-SERVER.md | 2 + open-sse/mcp-server/fetchTimeout.ts | 60 ++++++ open-sse/mcp-server/server.ts | 11 +- .../mcp-upstream-fetch-timeout-9717.test.ts | 191 ++++++++++++++++++ 5 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md create mode 100644 open-sse/mcp-server/fetchTimeout.ts create mode 100644 tests/unit/mcp-upstream-fetch-timeout-9717.test.ts diff --git a/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md b/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md new file mode 100644 index 0000000000..a23aeed2a1 --- /dev/null +++ b/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md @@ -0,0 +1 @@ +- **fix(mcp):** MCP tool calls that wait on a model provider no longer abort after 10 seconds. `omniRouteFetch` applied a single hardcoded `AbortSignal.timeout(10000)` to every internal hop, and `omniroute_route_request` — which posts to `/v1/chat/completions` and waits on the upstream provider, plus auto-combo candidate probing before a provider is even chosen — passed no signal of its own, so it inherited it. Any route slower than 10s failed from the MCP side while the identical request succeeded through the REST API. `omniroute_web_search` and `omniroute_web_fetch` in the same file already carried an explicit 60s signal, so that value is now shared by all three provider-bound calls instead of being repeated as a literal, while management reads (health, resilience, rate limits, combos, quota, usage) keep their fast-fail 10s budget so a stalled local endpoint still cannot hold a tool call open. Both budgets are overridable through `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` and `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS`, replacing the reported workaround of patching the compiled `dist/.build/next/server/chunks/*.js`; a malformed or non-positive override falls back to the default rather than disabling the timeout diff --git a/docs/frameworks/MCP-SERVER.md b/docs/frameworks/MCP-SERVER.md index 67e946528f..18877057fd 100644 --- a/docs/frameworks/MCP-SERVER.md +++ b/docs/frameworks/MCP-SERVER.md @@ -347,6 +347,8 @@ per-key path take precedence once it is. stdio has no per-caller identity (see | `OMNIROUTE_MCP_SCOPES` | (empty) | Comma-separated allowlist of scopes considered "available" by default (used when caller does not provide its own scopes) | | `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | (unset = on) | When set to `0/false/off/no`, disables MCP description compression at registration time | | `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | (unset = on) | Alternate alias for the same toggle as above | +| `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` | `10000` | Abort budget for internal management reads (health, resilience, combos, quota, usage) | +| `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS` | `60000` | Abort budget for hops that wait on a provider (`route_request`, `web_search`, `web_fetch`) | | `MCP_TOOL_DENY` | (unset = no filter) | Comma-separated tool names to drop from `tools/list` (tool-cardinality reduction — see below) | | `MCP_TOOL_ALLOW` | (unset = no filter) | Comma-separated tool names to keep exclusively (allow-list mode — see below) | | `DATA_DIR` | `~/.omniroute` | Heartbeat file is written to `${DATA_DIR}/runtime/mcp-heartbeat.json` | diff --git a/open-sse/mcp-server/fetchTimeout.ts b/open-sse/mcp-server/fetchTimeout.ts new file mode 100644 index 0000000000..a9389b0c24 --- /dev/null +++ b/open-sse/mcp-server/fetchTimeout.ts @@ -0,0 +1,60 @@ +/** + * #9717 — timeout policy for the MCP server's internal server→server fetches. + * + * `omniRouteFetch` serves two call shapes with very different latency budgets: + * fast local management reads (health, resilience, combos, quota, usage) and + * calls that wait on an upstream provider. A single 10s default aborted + * `omniroute_route_request` while the upstream request was still in flight, + * even though `omniroute_web_search` / `omniroute_web_fetch` already carried + * their own explicit 60s signal in the same file for exactly that reason. + * + * Kept as a pure, dependency-free module so the policy is unit-testable without + * starting the MCP server, mirroring how `tools/poolTools.ts` keeps handlers + * separate from server wiring. + */ + +/** Local management reads — a stalled one should fail fast, not hold a tool call open. */ +export const MCP_FETCH_TIMEOUT_MS = 10_000; + +/** + * Calls that wait on an upstream provider. 60s is not a new number: it is the + * value `web_search`/`web_fetch` already used, now shared with model routing + * instead of each call site picking its own literal. + */ +export const MCP_UPSTREAM_FETCH_TIMEOUT_MS = 60_000; + +export const MCP_FETCH_TIMEOUT_ENV = "OMNIROUTE_MCP_FETCH_TIMEOUT_MS"; +export const MCP_UPSTREAM_FETCH_TIMEOUT_ENV = "OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS"; + +export type McpFetchTimeoutKind = "management" | "upstream"; + +function readPositiveIntEnv(raw: string | undefined): number | null { + if (typeof raw !== "string" || raw.trim() === "") return null; + const parsed = Number(raw); + return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null; +} + +/** + * Resolve the timeout for one internal fetch class. An unset, malformed or + * non-positive override falls back to the built-in default rather than + * disabling the timeout — a bad env value must not turn a bounded wait into an + * unbounded one. + */ +export function resolveMcpFetchTimeoutMs( + kind: McpFetchTimeoutKind, + env: Record = process.env +): number { + const upstream = kind === "upstream"; + const override = readPositiveIntEnv( + env[upstream ? MCP_UPSTREAM_FETCH_TIMEOUT_ENV : MCP_FETCH_TIMEOUT_ENV] + ); + return override ?? (upstream ? MCP_UPSTREAM_FETCH_TIMEOUT_MS : MCP_FETCH_TIMEOUT_MS); +} + +/** `AbortSignal` for one internal fetch of the given class. */ +export function mcpFetchTimeoutSignal( + kind: McpFetchTimeoutKind, + env?: Record +): AbortSignal { + return AbortSignal.timeout(resolveMcpFetchTimeoutMs(kind, env)); +} diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index f5634816e1..b2a866e997 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -92,6 +92,7 @@ import { getDbInstance } from "../../src/lib/db/core.ts"; import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts"; import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts"; import { sanitizeErrorMessage } from "../utils/error.ts"; +import { mcpFetchTimeoutSignal } from "./fetchTimeout.ts"; import { getMcpModelsCatalog } from "./catalog.ts"; import { registerRadarCatalogTool } from "./radarCatalog.ts"; import type { TextToolResult } from "./toolResult.ts"; @@ -213,7 +214,7 @@ export async function omniRouteFetch(path: string, options: RequestInit = {}): P ...getInternalServiceAuthHeaders(), }; - const signal = options.signal || AbortSignal.timeout(10000); + const signal = options.signal || mcpFetchTimeoutSignal("management"); const response = await fetch(url, { ...options, headers, signal }); if (!response.ok) { @@ -518,6 +519,10 @@ async function handleRouteRequest(args: { const raw = (await omniRouteFetch("/v1/chat/completions", { method: "POST", body: JSON.stringify(body), + // #9717: this hop waits on an upstream provider (and on auto-combo + // candidate probing before one is even chosen), so it must not inherit + // the management-read budget. + signal: mcpFetchTimeoutSignal("upstream"), })) as JsonRecord; const choices = toArray(raw.choices); const firstChoice = toRecord(choices[0]); @@ -648,7 +653,7 @@ async function handleWebSearch(args: { const result = await omniRouteFetch("/v1/search", { method: "POST", body: JSON.stringify(body), - signal: AbortSignal.timeout(60000), + signal: mcpFetchTimeoutSignal("upstream"), }); await logToolCall("omniroute_web_search", args, result, Date.now() - start, true); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; @@ -681,7 +686,7 @@ async function handleWebFetch(args: { const result = await omniRouteFetch("/v1/web/fetch", { method: "POST", body: JSON.stringify(body), - signal: AbortSignal.timeout(60000), + signal: mcpFetchTimeoutSignal("upstream"), }); await logToolCall("omniroute_web_fetch", args, result, Date.now() - start, true); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; diff --git a/tests/unit/mcp-upstream-fetch-timeout-9717.test.ts b/tests/unit/mcp-upstream-fetch-timeout-9717.test.ts new file mode 100644 index 0000000000..270f56ea4f --- /dev/null +++ b/tests/unit/mcp-upstream-fetch-timeout-9717.test.ts @@ -0,0 +1,191 @@ +/** + * #9717 — the MCP server's internal fetch budget. + * + * `omniRouteFetch` applied one hardcoded 10s `AbortSignal.timeout` to every + * internal hop, including `omniroute_route_request`'s call to + * `/v1/chat/completions`. That hop waits on an upstream provider (and on + * auto-combo candidate probing before a provider is even chosen), so any route + * slower than 10s aborted from the MCP side while the same request succeeded + * through the REST API. `web_search`/`web_fetch` in the same file already used + * an explicit 60s signal, which is the value adopted here. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { + resolveMcpFetchTimeoutMs, + MCP_FETCH_TIMEOUT_MS, + MCP_UPSTREAM_FETCH_TIMEOUT_MS, + MCP_FETCH_TIMEOUT_ENV, + MCP_UPSTREAM_FETCH_TIMEOUT_ENV, +} = await import("../../open-sse/mcp-server/fetchTimeout.ts"); +const { createMcpServer, omniRouteFetch } = await import("../../open-sse/mcp-server/server.ts"); + +type RegisteredTool = { + handler: ( + args: unknown, + extra?: unknown + ) => Promise<{ content?: Array<{ type: string; text: string }>; isError?: boolean }>; +}; + +function getRegisteredHandler(server: unknown, toolName: string) { + const registry = (server as { _registeredTools?: Record }) + ._registeredTools; + assert.ok(registry, "McpServer should expose _registeredTools"); + const tool = registry[toolName]; + assert.ok(tool, `${toolName} must be registered on the live MCP server`); + return tool.handler; +} + +const CHAT_COMPLETION_BODY = { + choices: [{ message: { content: "ok" } }], + model: "test-model", + usage: { prompt_tokens: 1, completion_tokens: 1 }, + provider: "test-provider", +}; + +/** + * Stand-in for `fetch` that answers after `delayMs` but honours an abort signal + * the same way the real implementation does — a stub that ignored the signal + * would make every timeout assertion below pass vacuously. + */ +function stubFetch(delayMs: number, seen: { signals: AbortSignal[] }) { + const original = globalThis.fetch; + globalThis.fetch = ((_url: unknown, init?: { signal?: AbortSignal }) => { + const signal = init?.signal; + if (signal) seen.signals.push(signal); + return new Promise((resolve, reject) => { + const timer = setTimeout( + () => + resolve({ + ok: true, + status: 200, + json: async () => CHAT_COMPLETION_BODY, + text: async () => JSON.stringify(CHAT_COMPLETION_BODY), + }), + delayMs + ); + const abort = () => { + clearTimeout(timer); + reject(signal?.reason ?? new Error("aborted")); + }; + if (signal?.aborted) return abort(); + signal?.addEventListener("abort", abort, { once: true }); + }); + }) as typeof globalThis.fetch; + return () => { + globalThis.fetch = original; + }; +} + +function withEnv(vars: Record) { + const previous = new Map(); + for (const [key, value] of Object.entries(vars)) { + previous.set(key, process.env[key]); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + return () => { + for (const [key, value] of previous) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + }; +} + +async function callRouteRequest() { + const handler = getRegisteredHandler(createMcpServer(), "omniroute_route_request"); + return handler( + { model: "test-model", messages: [{ role: "user", content: "hi" }] }, + { authInfo: { clientId: "test-9717", scopes: ["execute:completions"] } } + ); +} + +// ── Policy ──────────────────────────────────────────────────────────────── + +test("#9717: the upstream budget is larger than the management budget", () => { + assert.equal(resolveMcpFetchTimeoutMs("management"), MCP_FETCH_TIMEOUT_MS); + assert.equal(resolveMcpFetchTimeoutMs("upstream"), MCP_UPSTREAM_FETCH_TIMEOUT_MS); + assert.equal(MCP_FETCH_TIMEOUT_MS, 10_000); + assert.equal( + MCP_UPSTREAM_FETCH_TIMEOUT_MS, + 60_000, + "matches the signal web_search/web_fetch already used" + ); + assert.ok(MCP_UPSTREAM_FETCH_TIMEOUT_MS > MCP_FETCH_TIMEOUT_MS); +}); + +test("#9717: each budget reads its own env override", () => { + const env = { + [MCP_FETCH_TIMEOUT_ENV]: "1234", + [MCP_UPSTREAM_FETCH_TIMEOUT_ENV]: "222222", + }; + assert.equal(resolveMcpFetchTimeoutMs("management", env), 1234); + assert.equal(resolveMcpFetchTimeoutMs("upstream", env), 222222); +}); + +test("#9717: a malformed override falls back to the default instead of disabling the timeout", () => { + for (const bad of ["", " ", "0", "-1", "abc", "60000.5", "NaN", "Infinity"]) { + assert.equal( + resolveMcpFetchTimeoutMs("upstream", { [MCP_UPSTREAM_FETCH_TIMEOUT_ENV]: bad }), + MCP_UPSTREAM_FETCH_TIMEOUT_MS, + `"${bad}" must not become the effective timeout` + ); + } +}); + +// ── Wiring ──────────────────────────────────────────────────────────────── + +test("#9717: route_request is bound to the upstream budget, not the management default", async () => { + const seen = { signals: [] as AbortSignal[] }; + const restoreEnv = withEnv({ [MCP_UPSTREAM_FETCH_TIMEOUT_ENV]: "40" }); + const restoreFetch = stubFetch(400, seen); + try { + const result = await callRouteRequest(); + assert.equal( + result.isError, + true, + "with the upstream budget set to 40ms a 400ms upstream must abort — before #9717 this " + + "call ignored that setting and used the hardcoded 10s default, so it returned a result" + ); + assert.ok(seen.signals.length > 0, "the routing hop must carry an abort signal"); + } finally { + restoreFetch(); + restoreEnv(); + } +}); + +test("#9717: route_request outlives the management budget", async () => { + const seen = { signals: [] as AbortSignal[] }; + const restoreEnv = withEnv({ + [MCP_FETCH_TIMEOUT_ENV]: "40", + [MCP_UPSTREAM_FETCH_TIMEOUT_ENV]: undefined, + }); + const restoreFetch = stubFetch(400, seen); + try { + const result = await callRouteRequest(); + assert.notEqual( + result.isError, + true, + "a 400ms upstream must survive: the routing hop must not inherit the 40ms management budget" + ); + } finally { + restoreFetch(); + restoreEnv(); + } +}); + +test("#9717: management reads honour their own override", async () => { + const seen = { signals: [] as AbortSignal[] }; + const restoreEnv = withEnv({ [MCP_FETCH_TIMEOUT_ENV]: "40" }); + const restoreFetch = stubFetch(400, seen); + try { + await assert.rejects( + () => omniRouteFetch("/api/monitoring/health"), + "a management read must abort at its configured budget" + ); + } finally { + restoreFetch(); + restoreEnv(); + } +}); From a280bfc11229e4afb71e45b3197621e79ed7553b Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:19:59 +0700 Subject: [PATCH 091/135] fix(context): budget base64 file payloads instead of counting them as text (#10858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — um PDF de ~1MB enviado como file/document base64 (OpenAI ou Claude) era medido caractere-a-caractere, estimando 350.022 tokens (o mesmo documento pelo path Gemini inlineData já estimava 1.209). Corrige a inconsistência reconhecendo os shapes que faltavam, sem introduzir constante nova. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/10840-file-token-context.test.ts — 5/5 passando - Suítes de contexto relacionadas — 59/59 (5 arquivos) passando --- .../fixes/10858-base64-file-token-estimate.md | 1 + open-sse/services/contextManager.ts | 56 ++++++++++ tests/unit/10840-file-token-context.test.ts | 101 ++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 changelog.d/fixes/10858-base64-file-token-estimate.md create mode 100644 tests/unit/10840-file-token-context.test.ts diff --git a/changelog.d/fixes/10858-base64-file-token-estimate.md b/changelog.d/fixes/10858-base64-file-token-estimate.md new file mode 100644 index 0000000000..18d8104b10 --- /dev/null +++ b/changelog.d/fixes/10858-base64-file-token-estimate.md @@ -0,0 +1 @@ +- **fix(context):** Base64 file payloads (OpenAI `file` parts, Responses `input_file`, Claude `document` blocks) are budgeted like the Gemini `inlineData` path instead of being counted as prompt text — a ~1MB PDF estimated at 350k tokens and was rejected on the context limit before reaching the provider's document pipeline ([#10840](https://github.com/diegosouzapw/OmniRoute/issues/10840), [#10858](https://github.com/diegosouzapw/OmniRoute/pull/10858)) — thanks @ntdat812 diff --git a/open-sse/services/contextManager.ts b/open-sse/services/contextManager.ts index c231759a07..a2d678f107 100644 --- a/open-sse/services/contextManager.ts +++ b/open-sse/services/contextManager.ts @@ -75,6 +75,13 @@ const CHARS_PER_TOKEN = 4; // see #8368 research notes. const IMAGE_TOKEN_ESTIMATE = 1200; +// #10840: same budget, deliberately. The Gemini `inlineData` matcher does not +// inspect media type, so a base64 PDF arriving in that shape is ALREADY measured +// at IMAGE_TOKEN_ESTIMATE today. Reusing it makes the OpenAI `file` and Claude +// `document` shapes agree with the estimate the same document already receives, +// rather than introducing a second constant with no grounding in this repo. +const DOCUMENT_TOKEN_ESTIMATE = IMAGE_TOKEN_ESTIMATE; + // Matches inline base64 data URLs, e.g. "data:image/png;base64,AAAA...". // Deliberately scoped to `data:image/...;base64,` so remote (http/https) // URLs and generic long base64 text strings stay on the text-estimation path. @@ -117,6 +124,45 @@ function matchesGeminiInlineDataShape(node: Record): boolean { return typeof (inlineData as Record).data === "string"; } +// Any inline base64 data URL, regardless of media type — file parts legitimately +// carry application/pdf, text/csv, and so on. +const INLINE_BASE64_DATA_RE = /^data:[^;,]+;base64,/; + +function isInlineBase64DataUrl(value: unknown): boolean { + return typeof value === "string" && INLINE_BASE64_DATA_RE.test(value); +} + +// OpenAI chat.completions: { type: 'file', file: { file_data | data: 'data:...;base64,...' } } +// Responses API: { type: 'input_file', file_data: 'data:...;base64,...' } +// Shapes mirror services/ccOpenAiMediaBlocks.ts::convertOpenAiMediaBlock. +function matchesOpenAIFileShape(node: Record): boolean { + if (node.type === "input_file") return isInlineBase64DataUrl(node.file_data); + if (node.type !== "file") return false; + const file = node.file; + if (!file || typeof file !== "object") return false; + const f = file as Record; + return isInlineBase64DataUrl(f.file_data) || isInlineBase64DataUrl(f.data); +} + +// Claude: { type: 'document', source: { type: 'base64', data: '...' } } +function matchesClaudeDocumentShape(node: Record): boolean { + if (node.type !== "document") return false; + const source = node.source; + if (!source || typeof source !== "object") return false; + const src = source as Record; + return src.type === "base64" && typeof src.data === "string"; +} + +/** + * Detect inline-base64 *document* blocks (#10840). Deliberately separate from + * {@link isInlineBase64ImageBlock}: that predicate also drives + * pruneOlderInlineImages, and dropping a user's attached PDF is not the same + * decision as dropping an old screenshot. This one only feeds token estimation. + */ +export function isInlineBase64DocumentBlock(node: Record): boolean { + return matchesOpenAIFileShape(node) || matchesClaudeDocumentShape(node); +} + /** * Detect the 5 documented inline-base64 image content-block shapes (see the * shape-specific matchers above). @@ -224,6 +270,10 @@ function extractImageTokens(node: unknown, seen: Set): { node: unknown; tokens += IMAGE_TOKEN_ESTIMATE; return { __image_token_estimate__: IMAGE_TOKEN_ESTIMATE }; } + if (record && isInlineBase64DocumentBlock(record)) { + tokens += DOCUMENT_TOKEN_ESTIMATE; + return { __document_token_estimate__: DOCUMENT_TOKEN_ESTIMATE }; + } const result = extractImageTokens(item, seen); tokens += result.tokens; return result.node; @@ -238,6 +288,12 @@ function extractImageTokens(node: unknown, seen: Set): { node: unknown; tokens: IMAGE_TOKEN_ESTIMATE, }; } + if (isInlineBase64DocumentBlock(record)) { + return { + node: { __document_token_estimate__: DOCUMENT_TOKEN_ESTIMATE }, + tokens: DOCUMENT_TOKEN_ESTIMATE, + }; + } let tokens = 0; const out: Record = {}; diff --git a/tests/unit/10840-file-token-context.test.ts b/tests/unit/10840-file-token-context.test.ts new file mode 100644 index 0000000000..68bd0f0eb3 --- /dev/null +++ b/tests/unit/10840-file-token-context.test.ts @@ -0,0 +1,101 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + estimateTokens, + isInlineBase64DocumentBlock, + isInlineBase64ImageBlock, + pruneOlderInlineImages, +} from "../../open-sse/services/contextManager.ts"; + +/** + * #10840 — a base64 file payload (PDF and friends) was measured as ordinary + * prompt text, so large documents were rejected on the context limit before + * ever reaching a provider's native document pipeline. + * + * The estimate must not depend on which wire shape the document arrived in: + * the Gemini `inlineData` matcher never inspected media type, so the SAME PDF + * was already budgeted at the bounded image estimate there while the OpenAI + * `file` and Claude `document` shapes were measured character by character. + */ + +function base64Payload(approxBytes: number): string { + return Buffer.alloc(approxBytes, 65).toString("base64"); +} + +const PDF_B64 = base64Payload(1_000_000); // ~1 MB document +const PDF_DATA_URL = `data:application/pdf;base64,${PDF_B64}`; + +const SHAPES: Array<[string, Record]> = [ + ["gemini inlineData", { inlineData: { mimeType: "application/pdf", data: PDF_B64 } }], + [ + "claude document", + { type: "document", source: { type: "base64", media_type: "application/pdf", data: PDF_B64 } }, + ], + ["openai file.file_data", { type: "file", file: { filename: "d.pdf", file_data: PDF_DATA_URL } }], + ["openai file.data", { type: "file", file: { filename: "d.pdf", data: PDF_DATA_URL } }], + ["responses input_file", { type: "input_file", filename: "d.pdf", file_data: PDF_DATA_URL }], +]; + +test("#10840: a base64 document is never measured as raw prompt text", () => { + for (const [name, block] of SHAPES) { + const tokens = estimateTokens({ + messages: [{ role: "user", content: [{ type: "text", text: "Summarise this." }, block] }], + }); + assert.ok( + tokens < 5_000, + `${name}: expected a bounded document estimate, got ${tokens} tokens for a ~1MB file` + ); + } +}); + +test("#10840: every wire shape of the same document agrees", () => { + const counts = SHAPES.map(([, block]) => estimateTokens(block)); + const unique = [...new Set(counts)]; + assert.equal( + unique.length, + 1, + `the same document must cost the same regardless of shape, got ${JSON.stringify( + SHAPES.map(([n], i) => `${n}=${counts[i]}`) + )}` + ); +}); + +test("#10840: a remote file URL still flows through the text path", () => { + // Not base64 transport — nothing to exclude, and it is short anyway. + const block = { type: "file", file: { filename: "d.pdf", file_data: "https://x.test/d.pdf" } }; + assert.equal(isInlineBase64DocumentBlock(block), false); +}); + +test("#10840: document detection stays separate from image detection", () => { + const doc = SHAPES[2][1]; + const img = { type: "image_url", image_url: { url: "data:image/png;base64,AAAA" } }; + + assert.equal(isInlineBase64DocumentBlock(doc), true); + assert.equal(isInlineBase64ImageBlock(doc), false, "a document must not register as an image"); + assert.equal(isInlineBase64ImageBlock(img), true); + assert.equal(isInlineBase64DocumentBlock(img), false); +}); + +test("#10840: pruneOlderInlineImages still ignores documents", () => { + // Dropping an attached PDF is not the same decision as dropping an old + // screenshot, so the pruner must keep its image-only scope. + const messages = [ + { + role: "user", + content: [{ type: "file", file: { filename: "a.pdf", file_data: PDF_DATA_URL } }], + }, + { + role: "user", + content: [{ type: "file", file: { filename: "b.pdf", file_data: PDF_DATA_URL } }], + }, + { + role: "user", + content: [{ type: "file", file: { filename: "c.pdf", file_data: PDF_DATA_URL } }], + }, + ]; + + const { pruned, messages: after } = pruneOlderInlineImages(messages, { keepLatest: 1 }); + + assert.equal(pruned, 0, "documents must not be pruned by the image pruner"); + assert.deepEqual(after, messages); +}); From 3112304db688f15596c7fb3dfba7e8f45fd78193 Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:20:08 +0700 Subject: [PATCH 092/135] fix(catalog): stop advertising auto/* when auto routing is disabled (#10857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — /v1/models continuava anunciando 38 IDs auto/* mesmo com autoRoutingEnabled: false, todos garantidos a falhar em tempo de request (HTTP 400). Une a condição de ocultação ao hideAutoCombos já existente sem adicionar uma dimensão nova à cache-key (evita quebrar #10313). Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/catalog-auto-routing-disabled-10831.test.ts — 2/2 passando - Suítes catalog relacionadas (hide-auto-no-think, cache-key-hashing, eventloop-yield) — 9/9 passando --- ...-hide-auto-models-when-routing-disabled.md | 1 + src/app/api/v1/models/catalog.ts | 12 ++- ...atalog-auto-routing-disabled-10831.test.ts | 98 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md create mode 100644 tests/unit/catalog-auto-routing-disabled-10831.test.ts diff --git a/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md new file mode 100644 index 0000000000..39da596ee3 --- /dev/null +++ b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md @@ -0,0 +1 @@ +- **fix(catalog):** `/v1/models` no longer advertises the built-in `auto/*` ids while auto routing is disabled — they were listed but rejected at request time with `Auto routing is disabled` ([#10831](https://github.com/diegosouzapw/OmniRoute/issues/10831), [#10857](https://github.com/diegosouzapw/OmniRoute/pull/10857)) — thanks @ntdat812 diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index d4797775db..a864ca3608 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -189,7 +189,11 @@ export async function getUnifiedModelsResponse( { corsHeaders, diagnosticHeaders }, buildCatalogPayload, { - hideAutoCombos: settingsForAuth?.hideAutoCombos === true, + // #10831: a disabled router hides auto/* just as hideAutoCombos does, so + // the two collapse into one cache dimension — the resulting catalogs are + // identical and do not need separate entries. + hideAutoCombos: + settingsForAuth?.hideAutoCombos === true || settingsForAuth?.autoRoutingEnabled === false, hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true, } ); @@ -301,7 +305,11 @@ async function buildUnifiedModelsResponseCore( // #9418: Opt-in filter — skip the entire auto/* synthesis loop when the operator // does not want built-in virtual combos advertised in the catalog. User-defined // combos are unaffected; routing still works for ids sent explicitly. - const hideAuto = settings.hideAutoCombos === true; + // #10831: also drop them when auto routing is switched off. Unlike + // hideAutoCombos — which only unadvertises ids that still route when sent + // explicitly — a disabled router rejects every auto/* id with a 400, so + // listing them offers the client a choice that cannot succeed. + const hideAuto = settings.hideAutoCombos === true || settings.autoRoutingEnabled === false; const shouldHidePaid = (providerKey: string, modelId: string, pricing?: unknown): boolean => { if (!hidePaid) return false; const provider = aliasToProviderId[providerKey] || providerKey; diff --git a/tests/unit/catalog-auto-routing-disabled-10831.test.ts b/tests/unit/catalog-auto-routing-disabled-10831.test.ts new file mode 100644 index 0000000000..f2f96ef95f --- /dev/null +++ b/tests/unit/catalog-auto-routing-disabled-10831.test.ts @@ -0,0 +1,98 @@ +/** + * #10831 — when auto routing is switched off, `auto/*` ids must not be + * advertised in `/v1/models`. + * + * Unlike `hideAutoCombos` (#9418), which only unadvertises ids that still route + * when a client sends them explicitly, `autoRoutingEnabled: false` makes the + * router reject every `auto/*` id with + * "Auto routing is disabled. Enable it in Settings > Routing." (see + * src/sse/handlers/autoRouting.ts). Listing them therefore offers the picker a + * choice that can only fail. + */ +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-auto-routing-10831-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); + +async function fetchCatalog(): Promise> { + const res = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models", { method: "GET" }) + ); + if (res.status !== 200) { + const body = await res.text(); + assert.fail(`Expected 200, got ${res.status}: ${body.slice(0, 500)}`); + } + const body = (await res.json()) as { data: Array<{ id: string }> }; + return body.data; +} + +const isAutoId = (m: { id: string }) => m.id.startsWith("auto/"); + +test.after(() => { + core.resetDbInstance(); + try { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } catch { + /* best-effort */ + } +}); + +test("autoRoutingEnabled=false removes auto/* ids from /v1/models", async () => { + await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "openai-main", + apiKey: "sk-test", + isActive: true, + }); + + // Baseline: routing on, ids advertised. + await settingsDb.updateSettings({ autoRoutingEnabled: true, hideAutoCombos: false }); + const on = await fetchCatalog(); + const autoWhenOn = on.filter(isAutoId).map((m) => m.id); + assert.equal( + autoWhenOn.length > 0, + true, + `expected auto/* ids while auto routing is enabled, got ${autoWhenOn.length}` + ); + + // Routing off: none may remain. + await settingsDb.updateSettings({ autoRoutingEnabled: false, hideAutoCombos: false }); + const off = await fetchCatalog(); + const leaked = off.filter(isAutoId).map((m) => m.id); + assert.deepEqual( + leaked, + [], + `auto/* ids leaked while auto routing is disabled: ${leaked.join(", ")}` + ); + + // Everything else must survive — this is a filter, not a catalog wipe. + const hasProviderModel = off.some((m) => m.id.startsWith("openai/") || m.id.startsWith("oa/")); + assert.equal(hasProviderModel, true, "provider models must remain when auto routing is disabled"); +}); + +test("re-enabling auto routing brings auto/* ids back (cache key varies on the flag)", async () => { + await settingsDb.updateSettings({ autoRoutingEnabled: false, hideAutoCombos: false }); + const off = await fetchCatalog(); + assert.deepEqual( + off.filter(isAutoId).map((m) => m.id), + [] + ); + + await settingsDb.updateSettings({ autoRoutingEnabled: true, hideAutoCombos: false }); + const back = await fetchCatalog(); + assert.equal( + back.filter(isAutoId).length > 0, + true, + "auto/* ids must return once auto routing is re-enabled — a stale cached catalog would fail here" + ); +}); From 053c64d380f88b28c7624a1376eb4397f8839e19 Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:20:19 +0700 Subject: [PATCH 093/135] fix(i18n): stop rendering the Disabled status as "person with a disability" (#10853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — corrige um erro sério de tradução em 8 locales, onde o status "Disabled" era traduzido pelo substantivo "pessoa com deficiência" (24 strings), estendendo o escopo original do #10812 (só japonês) para todos os locales afetados. Cada substituição usa o termo que o próprio catálogo já emprega para a mesma fonte em inglês — nenhuma terminologia nova introduzida. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/i18n-disabled-not-person-with-disability.test.ts — 1/1 passando (varre todos os locales) - Glossary gates (zh-CN, zh-TW, ko) — PASS; i18n:check-ui-coverage e check-value-drift — PASS - Duas camadas de proteção contra regressão: glossário (zh-CN/zh-TW) + teste catalog-wide cobrindo 21 termos --- .../10853-i18n-disabled-mistranslation.md | 1 + scripts/i18n/glossary/zh-CN.json | 4 + scripts/i18n/glossary/zh-TW.json | 4 + src/i18n/messages/es.json | 10 +- src/i18n/messages/hi.json | 10 +- src/i18n/messages/ja.json | 8 +- src/i18n/messages/pl.json | 2 +- src/i18n/messages/te.json | 4 +- src/i18n/messages/ur.json | 4 +- src/i18n/messages/zh-CN.json | 4 +- src/i18n/messages/zh-TW.json | 6 +- ...isabled-not-person-with-disability.test.ts | 99 +++++++++++++++++++ 12 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 changelog.d/fixes/10853-i18n-disabled-mistranslation.md create mode 100644 tests/unit/i18n-disabled-not-person-with-disability.test.ts diff --git a/changelog.d/fixes/10853-i18n-disabled-mistranslation.md b/changelog.d/fixes/10853-i18n-disabled-mistranslation.md new file mode 100644 index 0000000000..836cc6491e --- /dev/null +++ b/changelog.d/fixes/10853-i18n-disabled-mistranslation.md @@ -0,0 +1 @@ +- **fix(i18n):** The "Disabled" status no longer renders as the noun for a person with a disability in Japanese, Spanish, Hindi, Polish, Telugu, Urdu and both Chinese locales — 24 strings now use each catalog's existing wording (ja 無効, es Deshabilitado, hi अक्षम, pl Wyłączone, te నిలిపివేయబడింది, ur غیر فعال, zh-CN 已禁用, zh-TW 已停用) ([#10812](https://github.com/diegosouzapw/OmniRoute/issues/10812), [#10853](https://github.com/diegosouzapw/OmniRoute/pull/10853)) — thanks @ntdat812 diff --git a/scripts/i18n/glossary/zh-CN.json b/scripts/i18n/glossary/zh-CN.json index 3af6e67c95..b56a79ed92 100644 --- a/scripts/i18n/glossary/zh-CN.json +++ b/scripts/i18n/glossary/zh-CN.json @@ -42,6 +42,10 @@ "circuit breaker": { "canonical": "断路器", "synonyms": [] + }, + "disabled (status)": { + "canonical": "已禁用", + "synonyms": ["残疾人"] } } } diff --git a/scripts/i18n/glossary/zh-TW.json b/scripts/i18n/glossary/zh-TW.json index 39b9c3ad1f..0e4000d8c5 100644 --- a/scripts/i18n/glossary/zh-TW.json +++ b/scripts/i18n/glossary/zh-TW.json @@ -78,6 +78,10 @@ "canonical": "專案", "synonyms": [], "note": "Enforcement deferred: 項目 is also the correct rendering of 'item' (依賴項目, 必要項目, 共通項目), which dominates real usage. Only 項目概覽 -> 專案概覽 is normalized by hand." + }, + "disabled (status)": { + "canonical": "已停用", + "synonyms": ["殘疾人", "殘障人士"] } } } diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 6978c01a41..6b460d7a8f 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -5111,7 +5111,7 @@ "lastFailureSuffix": "__MISSING__: (last failure {time})", "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" }, - "disabled": "Discapacitado", + "disabled": "Deshabilitado", "enableProvider": "Habilitar proveedor", "disableProvider": "Deshabilitar proveedor", "testResults": "Resultados de la prueba", @@ -5335,7 +5335,7 @@ "builtInModels": "Built-in models", "builtInModelsHint": "Registry models for this provider. Use the pencil to set compatibility options.", "pageAutoRefresh": "La página se actualizará automáticamente...", - "statusDisabled": "discapacitado", + "statusDisabled": "deshabilitado", "statusConnected": "conectado", "statusRuntimeIssue": "problema de tiempo de ejecución", "statusAuthFailed": "autenticación fallida", @@ -6745,7 +6745,7 @@ "global": "Mundial", "rule": "regla", "enabled": "Habilitado", - "disabled": "Discapacitado", + "disabled": "Deshabilitado", "nodeCount": "Nodos: {count}", "needsCoreCount": "{count} necesita núcleo local", "lastSynced": "Última sincronización: {time}", @@ -7016,7 +7016,7 @@ "systemActor": "sistema", "ipAccessControl": "Control de acceso IP", "ipAccessControlDesc": "Bloquear o permitir direcciones IP específicas", - "ipModeDisabled": "Discapacitado", + "ipModeDisabled": "Deshabilitado", "ipModeBlacklist": "Lista negra", "ipModeWhitelist": "Lista blanca", "ipModeWhitelistPriority": "Prioridad WL", @@ -7940,7 +7940,7 @@ "triggerLabel": "gatillo", "effectLabel": "Efecto", "statusEnabled": "Habilitado", - "statusDisabled": "Discapacitado", + "statusDisabled": "Deshabilitado", "resilienceRequestQueueScope": "Por cola de solicitudes", "resilienceRequestQueueTrigger": "Antes de enviar al upstream", "resilienceRequestQueueEffect": "Pone en cola las solicitudes, limita la simultaneidad y espacia las llamadas", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 6591311189..bf73a186fc 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -27,7 +27,7 @@ "copy": "प्रतिलिपि", "copied": "नकल की गई!", "enabled": "सक्षम", - "disabled": "विकलांग", + "disabled": "अक्षम", "active": "सक्रिय", "inactive": "निष्क्रिय", "noData": "कोई डेटा उपलब्ध नहीं है", @@ -5111,7 +5111,7 @@ "lastFailureSuffix": "__MISSING__: (last failure {time})", "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" }, - "disabled": "विकलांग", + "disabled": "अक्षम", "enableProvider": "प्रदाता सक्षम करें", "disableProvider": "प्रदाता को अक्षम करें", "testResults": "परीक्षण के परिणाम", @@ -6745,7 +6745,7 @@ "global": "वैश्विक", "rule": "नियम", "enabled": "सक्षम", - "disabled": "विकलांग", + "disabled": "अक्षम", "nodeCount": "नोड्स: {count}", "needsCoreCount": "{count} को स्थानीय कोर की आवश्यकता है", "lastSynced": "अंतिम बार समन्वयित: {time}", @@ -7016,7 +7016,7 @@ "systemActor": "प्रणाली", "ipAccessControl": "आईपी ​​अभिगम नियंत्रण", "ipAccessControlDesc": "विशिष्ट आईपी पते को ब्लॉक करें या अनुमति दें", - "ipModeDisabled": "विकलांग", + "ipModeDisabled": "अक्षम", "ipModeBlacklist": "काली सूची", "ipModeWhitelist": "श्वेतसूची", "ipModeWhitelistPriority": "डब्ल्यूएल प्राथमिकता", @@ -7940,7 +7940,7 @@ "triggerLabel": "ट्रिगर", "effectLabel": "प्रभाव", "statusEnabled": "सक्षम", - "statusDisabled": "विकलांग", + "statusDisabled": "अक्षम", "resilienceRequestQueueScope": "प्रति अनुरोध कतार", "resilienceRequestQueueTrigger": "अपस्ट्रीम पर भेजने से पहले", "resilienceRequestQueueEffect": "अनुरोधों को कतारबद्ध करता है, समवर्तीता को सीमित करता है, और कॉलों को रिक्त स्थान देता है", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index a8c37c9eec..7a12061df8 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -5111,7 +5111,7 @@ "lastFailureSuffix": "__MISSING__: (last failure {time})", "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" }, - "disabled": "障害者", + "disabled": "無効", "enableProvider": "プロバイダーを有効にする", "disableProvider": "プロバイダーを無効にする", "testResults": "テスト結果", @@ -6745,7 +6745,7 @@ "global": "グローバル", "rule": "ルール", "enabled": "有効", - "disabled": "障害者", + "disabled": "無効", "nodeCount": "ノード: {count}", "needsCoreCount": "{count} にはローカル コアが必要です", "lastSynced": "最終同期: {time}", @@ -7016,7 +7016,7 @@ "systemActor": "システム", "ipAccessControl": "IPアクセス制御", "ipAccessControlDesc": "特定の IP アドレスをブロックまたは許可する", - "ipModeDisabled": "障害者", + "ipModeDisabled": "無効", "ipModeBlacklist": "ブラックリスト", "ipModeWhitelist": "ホワイトリスト", "ipModeWhitelistPriority": "WL優先", @@ -7940,7 +7940,7 @@ "triggerLabel": "トリガー", "effectLabel": "効果", "statusEnabled": "有効", - "statusDisabled": "障害者", + "statusDisabled": "無効", "resilienceRequestQueueScope": "リクエストキューごと", "resilienceRequestQueueTrigger": "上流に送る前に", "resilienceRequestQueueEffect": "リクエストをキューに入れ、同時実行を制限し、呼び出しの間隔を空けます。", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 04590ea8ad..7fb9dbc363 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -6745,7 +6745,7 @@ "global": "Globalny", "rule": "Reguła", "enabled": "Włączone", - "disabled": "Niepełnosprawny", + "disabled": "Wyłączone", "nodeCount": "Węzły: {count}", "needsCoreCount": "{count} potrzebuje lokalnego rdzenia", "lastSynced": "Ostatnia synchronizacja: {time}", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 65aa08d437..58688a4858 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -6745,7 +6745,7 @@ "global": "గ్లోబల్", "rule": "నియమం", "enabled": "ప్రారంభించబడింది", - "disabled": "వికలాంగుడు", + "disabled": "నిలిపివేయబడింది", "nodeCount": "నోడ్స్: {count}", "needsCoreCount": "{count}కి లోకల్ కోర్ అవసరం", "lastSynced": "చివరిగా సమకాలీకరించబడినది: {time}", @@ -7940,7 +7940,7 @@ "triggerLabel": "ట్రిగ్గర్", "effectLabel": "ప్రభావం", "statusEnabled": "ప్రారంభించబడింది", - "statusDisabled": "వికలాంగుడు", + "statusDisabled": "నిలిపివేయబడింది", "resilienceRequestQueueScope": "ప్రతి అభ్యర్థన క్యూ", "resilienceRequestQueueTrigger": "అప్‌స్ట్రీమ్‌కు పంపే ముందు", "resilienceRequestQueueEffect": "క్యూల అభ్యర్థనలు, సమ్మతిని పరిమితం చేస్తుంది మరియు కాల్‌లను ఖాళీ చేస్తుంది", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 22deb83299..76749d9d57 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -6745,7 +6745,7 @@ "global": "عالمی", "rule": "قاعدہ", "enabled": "فعال", - "disabled": "معذور", + "disabled": "غیر فعال", "nodeCount": "نوڈس: {count}", "needsCoreCount": "{count} کو مقامی کور کی ضرورت ہے۔", "lastSynced": "آخری بار مطابقت پذیری: {time}", @@ -7940,7 +7940,7 @@ "triggerLabel": "محرک", "effectLabel": "اثر", "statusEnabled": "فعال", - "statusDisabled": "معذور", + "statusDisabled": "غیر فعال", "resilienceRequestQueueScope": "فی درخواست کی قطار", "resilienceRequestQueueTrigger": "اپ اسٹریم پر بھیجنے سے پہلے", "resilienceRequestQueueEffect": "درخواستوں کو قطار میں لگاتا ہے، ہم آہنگی کو محدود کرتا ہے، اور کالوں کو ختم کرتا ہے۔", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 31d1a87e10..b2c15380aa 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -7437,7 +7437,7 @@ "qdrantDesc": "可选。在外部向量数据库中索引语义记忆以加快检索速度。", "qdrantStatusActive": "活跃", "qdrantStatusError": "错误", - "qdrantStatusDisabled": "残疾人", + "qdrantStatusDisabled": "已禁用", "qdrantEnable": "启用 Qdrant", "qdrantEnableDesc": "启用后,语义/混合策略可以使用 Qdrant 来检索记忆。", "qdrantTesting": "测试...", @@ -7944,7 +7944,7 @@ "triggerLabel": "触发", "effectLabel": "效果", "statusEnabled": "启用", - "statusDisabled": "残疾人", + "statusDisabled": "已禁用", "resilienceRequestQueueScope": "每个请求队列", "resilienceRequestQueueTrigger": "发送到上游之前", "resilienceRequestQueueEffect": "对请求进行排队、限制并发并间隔调用", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 6bc5f1954d..0da35d19db 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -6745,7 +6745,7 @@ "global": "全球", "rule": "規則", "enabled": "啟用", - "disabled": "殘障人士", + "disabled": "已停用", "nodeCount": "節點:{count}", "needsCoreCount": "{count} 需要本地核心", "lastSynced": "上次同步:{time}", @@ -7437,7 +7437,7 @@ "qdrantDesc": "可選。在外部向量資料庫中索引語義記憶以加快檢索速度。", "qdrantStatusActive": "活躍", "qdrantStatusError": "錯誤", - "qdrantStatusDisabled": "殘疾人", + "qdrantStatusDisabled": "已停用", "qdrantEnable": "啟用 Qdrant", "qdrantEnableDesc": "啟用後,語義/混合策略可以使用 Qdrant 來檢索記憶。", "qdrantTesting": "測試...", @@ -7944,7 +7944,7 @@ "triggerLabel": "觸發", "effectLabel": "效果", "statusEnabled": "啟用", - "statusDisabled": "殘疾人", + "statusDisabled": "已停用", "resilienceRequestQueueScope": "每個請求佇列", "resilienceRequestQueueTrigger": "傳送到上游之前", "resilienceRequestQueueEffect": "對請求進行排隊、限制併發並間隔呼叫", diff --git a/tests/unit/i18n-disabled-not-person-with-disability.test.ts b/tests/unit/i18n-disabled-not-person-with-disability.test.ts new file mode 100644 index 0000000000..19acc194c2 --- /dev/null +++ b/tests/unit/i18n-disabled-not-person-with-disability.test.ts @@ -0,0 +1,99 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// Regression guard for #10812: several locales rendered the *status* "Disabled" +// with the noun for a person who has a disability (ja 障害者, es Discapacitado, +// hi विकलांग, …). It is wrong in context and, for a status badge on a provider +// row, needlessly offensive. +// +// The glossary gate (scripts/i18n/glossary/.json) already enforces this +// for ko/zh-CN/zh-TW, but it only runs for locales that have a glossary file. +// This test covers every locale in the catalog, so a machine-translation pass +// cannot reintroduce the term in an ungated language. + +const messagesDir = path.join( + path.dirname(fileURLToPath(import.meta.url)), + "../../src/i18n/messages" +); + +// Nouns meaning "a person with a disability". None of these is ever a correct +// rendering of the "Disabled" status, so they are checked against the value of +// every key whose English source is "Disable"/"Disabled". +const PERSON_WITH_DISABILITY_TERMS = [ + "障害者", // ja + "장애인", // ko + "残疾", // zh-CN + "殘疾", // zh-TW + "残障", // zh-CN + "殘障", // zh-TW + "discapacitad", // es + "minusvál", // es + "deficiente físic", // pt + "handicapé", // fr + "инвалид", // ru + "інвалід", // uk + "विकलांग", // hi + "వికలాంగ", // te + "معذور", // ur + "معاق", // ar + "niepełnospraw", // pl + "gehandicapt", // nl + "khuyết tật", // vi + "ผู้พิการ", // th + "נכה", // he +]; + +type Json = string | number | boolean | null | Json[] | { [k: string]: Json }; + +function flatten(value: Json, prefix = "", out = new Map()) { + if (typeof value === "string") { + out.set(prefix, value); + } else if (value && typeof value === "object" && !Array.isArray(value)) { + for (const [k, v] of Object.entries(value)) { + flatten(v as Json, prefix ? `${prefix}.${k}` : k, out); + } + } + return out; +} + +function load(locale: string) { + return flatten(JSON.parse(readFileSync(path.join(messagesDir, `${locale}.json`), "utf8"))); +} + +test("no locale renders the Disabled status as a person with a disability (#10812)", () => { + const en = load("en"); + const disabledKeys = [...en.entries()] + .filter( + ([, v]) => v.toLowerCase().replace(/\.$/, "") === "disabled" || v.toLowerCase() === "disable" + ) + .map(([k]) => k); + + assert.ok(disabledKeys.length > 0, "expected the en catalog to define Disable/Disabled keys"); + + const locales = readdirSync(messagesDir) + .filter((f) => f.endsWith(".json")) + .map((f) => f.slice(0, -".json".length)) + .filter((l) => l !== "en"); + + const violations: string[] = []; + for (const locale of locales) { + const messages = load(locale); + for (const key of disabledKeys) { + const value = messages.get(key); + if (!value) continue; + const hit = PERSON_WITH_DISABILITY_TERMS.find((term) => + value.toLowerCase().includes(term.toLowerCase()) + ); + if (hit) violations.push(`${locale} ${key} = ${JSON.stringify(value)} (contains ${hit})`); + } + } + + assert.deepEqual( + violations, + [], + `Disabled status mistranslated as a person with a disability:\n ${violations.join("\n ")}` + ); +}); From e5b7c40d11e6261b7618f81f3c5af88874c4254e Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Fri, 21 Aug 2026 01:20:29 +0700 Subject: [PATCH 094/135] fix(security): judge outbound hosts by address, not by spelling (#10843) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — fix de segurança real (reportado via GHSA-qcfj-c39q-88jh): isCloudMetadataHost() decidia por spelling dotted-decimal, então um literal IPv4-mapped IPv6 (ex.: [::ffff:169.254.169.254]) alcançava o guard já canonicalizado por new URL() e não era reconhecido como endpoint de metadata de cloud — bypass no modo que permite endpoints privados/LAN (o default local-first). Também fecha o gap equivalente de 0.0.0.0/::. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/outbound-guard-mapped-ipv4.test.ts — 12/12 passando (IMDS, Alibaba, ECS task role, ambas as grafias, hosts públicos, guard `::`) - Suítes SSRF relacionadas (webhook/firecrawl/kiro/provider-validation) — verdes --- .../fixes/10843-outbound-guard-mapped-ipv4.md | 1 + src/shared/network/outboundUrlGuard.ts | 34 +++++++- tests/unit/outbound-guard-mapped-ipv4.test.ts | 84 +++++++++++++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md create mode 100644 tests/unit/outbound-guard-mapped-ipv4.test.ts diff --git a/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md b/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md new file mode 100644 index 0000000000..2894ff65b0 --- /dev/null +++ b/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md @@ -0,0 +1 @@ +- **fix(security):** Outbound URL guard now resolves IPv4-mapped IPv6 literals to their embedded address, so `[::ffff:169.254.169.254]` is refused by the unconditional cloud-metadata block like its dotted spelling; `[::]` is refused alongside `0.0.0.0` ([#10843](https://github.com/diegosouzapw/OmniRoute/pull/10843)) — thanks @ntdat812 diff --git a/src/shared/network/outboundUrlGuard.ts b/src/shared/network/outboundUrlGuard.ts index 8a63de5be7..e7175da0bc 100644 --- a/src/shared/network/outboundUrlGuard.ts +++ b/src/shared/network/outboundUrlGuard.ts @@ -44,6 +44,9 @@ export function isPrivateHost(hostname: string) { if ( normalized === "localhost" || normalized === "0.0.0.0" || + // `::` is the IPv6 twin of `0.0.0.0`: connecting to it reaches a service bound + // to the IPv6 loopback, so it has to be refused alongside its IPv4 spelling. + normalized === "::" || normalized === "127.0.0.1" || normalized === "::1" || normalized.endsWith(".localhost") || @@ -81,6 +84,24 @@ export function isPrivateHost(hostname: string) { return false; } +// WHATWG URL serialises an IPv4-mapped IPv6 address as hextets, so +// `http://[::ffff:169.254.169.254]/` reaches these helpers as `::ffff:a9fe:a9fe`. +// Matching the dotted spelling alone therefore misses every mapped address that +// arrives through a parsed URL. Fold the embedded IPv4 back out before deciding. +function mappedIpv4Host(hostname: string): string | null { + const normalized = normalizeHost(hostname); + if (!normalized.startsWith("::ffff:")) return null; + const embedded = normalized.slice("::ffff:".length); + if (isIP(embedded) === 4) return embedded; + const hextets = embedded.split(":"); + if (hextets.length !== 2) return null; + const [high, low] = hextets.map((part) => + /^[0-9a-f]{1,4}$/.test(part) ? parseInt(part, 16) : Number.NaN + ); + if (Number.isNaN(high) || Number.isNaN(low)) return null; + return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`; +} + const CLOUD_METADATA_HOSTNAMES = new Set([ "169.254.169.254", // AWS / GCP / Azure / Oracle IMDS "metadata.google.internal", // GCP @@ -89,6 +110,11 @@ const CLOUD_METADATA_HOSTNAMES = new Set([ "fd00:ec2::254", // AWS IPv6 IMDS ]); +function isCloudMetadataIpv4(host: string): boolean { + if (CLOUD_METADATA_HOSTNAMES.has(host)) return true; + return host.startsWith("169.254."); // IPv4 link-local /16 +} + /** * Cloud-metadata and IPv4 link-local (169.254.0.0/16) endpoints are the classic * SSRF→IAM-credential pivot and have no legitimate webhook/automation use case. They are @@ -97,9 +123,11 @@ const CLOUD_METADATA_HOSTNAMES = new Set([ export function isCloudMetadataHost(hostname: string): boolean { const host = normalizeHost(hostname); if (!host) return false; - if (CLOUD_METADATA_HOSTNAMES.has(host)) return true; - if (host.startsWith("169.254.")) return true; // IPv4 link-local /16 - return false; + if (isCloudMetadataIpv4(host)) return true; + // An IPv4-mapped IPv6 literal routes to the embedded IPv4 address, so the same + // verdict has to apply to it — otherwise this block is spelling-sensitive. + const mapped = mappedIpv4Host(host); + return mapped !== null && isCloudMetadataIpv4(mapped); } export function parseOutboundUrl(input: string | URL) { diff --git a/tests/unit/outbound-guard-mapped-ipv4.test.ts b/tests/unit/outbound-guard-mapped-ipv4.test.ts new file mode 100644 index 0000000000..d7c005fe82 --- /dev/null +++ b/tests/unit/outbound-guard-mapped-ipv4.test.ts @@ -0,0 +1,84 @@ +/** + * Outbound URL guard: IPv4-mapped IPv6 coverage. + * + * `new URL()` serialises an IPv4-mapped IPv6 host as hextets + * (`[::ffff:169.254.169.254]` -> `[::ffff:a9fe:a9fe]`), so a guard that matches the + * dotted spelling never sees the address it is meant to reject. These tests pin the + * mapped spellings for both the unconditional cloud-metadata block and the + * private-host block, and pin `::` alongside its `0.0.0.0` twin. + * + * Run with: + * node --import tsx/esm --test tests/unit/outbound-guard-mapped-ipv4.test.ts + */ + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { + isCloudMetadataHost, + isPrivateHost, + parseAndValidateNonMetadataUrl, + parseAndValidatePublicUrl, + OutboundUrlGuardError, +} from "../../src/shared/network/outboundUrlGuard.ts"; + +// Each entry is the URL an attacker supplies and the address it actually routes to. +const MAPPED_METADATA_URLS = [ + ["http://[::ffff:169.254.169.254]/latest/meta-data/", "169.254.169.254 (AWS/GCP/Azure IMDS)"], + ["http://[::ffff:a9fe:a9fe]/latest/meta-data/", "169.254.169.254 via hextet spelling"], + ["http://[::ffff:100.100.100.200]/latest/meta-data/", "100.100.100.200 (Alibaba Cloud)"], + ["http://[::ffff:169.254.170.2]/v2/credentials", "169.254.170.2 (ECS task role)"], +] as const; + +describe("isCloudMetadataHost - IPv4-mapped IPv6", () => { + for (const [url, described] of MAPPED_METADATA_URLS) { + it(`treats ${new URL(url).hostname} as metadata (${described})`, () => { + assert.equal(isCloudMetadataHost(new URL(url).hostname), true); + }); + } + + it("still accepts public hosts", () => { + for (const host of [ + "api.openai.com", + "1.1.1.1", + "[2606:4700:4700::1111]", + "[::ffff:1.1.1.1]", + ]) { + assert.equal(isCloudMetadataHost(host), false, `${host} must not be treated as metadata`); + } + }); +}); + +describe("parseAndValidateNonMetadataUrl - metadata stays blocked when private URLs are allowed", () => { + // This guard mode intentionally permits LAN/loopback provider endpoints, so the + // cloud-metadata block is the only control standing between it and IMDS credentials. + for (const [url] of MAPPED_METADATA_URLS) { + it(`rejects ${url}`, () => { + assert.throws( + () => parseAndValidateNonMetadataUrl(url), + (error: unknown) => + error instanceof OutboundUrlGuardError && error.code === "OUTBOUND_URL_GUARD_BLOCKED" + ); + }); + } + + it("still allows a private LAN provider endpoint", () => { + assert.equal( + parseAndValidateNonMetadataUrl("http://192.168.1.50:11434/v1").hostname, + "192.168.1.50" + ); + }); +}); + +describe("isPrivateHost - unspecified address", () => { + it("blocks :: alongside 0.0.0.0", () => { + // Connecting to `::` reaches a service bound to the IPv6 loopback. + assert.equal(isPrivateHost("::"), true); + assert.equal(isPrivateHost("[::]"), true); + assert.equal(isPrivateHost("0.0.0.0"), true); + }); + + it("rejects http://[::]/ through the strict guard", () => { + assert.throws(() => parseAndValidatePublicUrl("http://[::]/"), OutboundUrlGuardError); + }); +}); From 2c84ce19dfae0bb1a49acd01a7f8ff53e87ad779 Mon Sep 17 00:00:00 2001 From: adevwithpurpose Date: Thu, 20 Aug 2026 23:34:00 +0500 Subject: [PATCH 095/135] fix(build): ensure standalone package.json declares module type for Node 24 worker compatibility (#10836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — corrige um warning real de runtime em produção sob Node.js 24: o package.json do standalone gerado pelo Next.js não declara "type": "module", forçando reparse de todo worker thread ESM (callLogArtifactWorker, onnxWorker) a cada spawn. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - Confirmado que colocate-standalone.mjs escreve "type": "module" corretamente; testado sob Node 24.19.0, workers sobem sem warning de reparse --- scripts/build/colocate-standalone.mjs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/build/colocate-standalone.mjs b/scripts/build/colocate-standalone.mjs index 736527b8dd..d0298893f7 100644 --- a/scripts/build/colocate-standalone.mjs +++ b/scripts/build/colocate-standalone.mjs @@ -13,7 +13,7 @@ * * Run manually after a build, or automatically via the `postbuild` npm hook. */ -import { cpSync, existsSync, mkdirSync } from "node:fs"; +import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; @@ -107,3 +107,22 @@ for (const pkg of closure) { console.log( `[colocate-standalone] ✅ optional-dep closure: ${closure.length} packages (copied ${copied})` ); + +// 3) Ensure standalone package.json declares "type": "module" so Node 24 runs ESM worker bundles without warning +const standalonePkgPath = join(STANDALONE, "package.json"); +if (existsSync(standalonePkgPath)) { + try { + const rawPkg = readFileSync(standalonePkgPath, "utf8"); + const pkgJson = JSON.parse(rawPkg); + if (!pkgJson.type) { + pkgJson.type = "module"; + writeFileSync(standalonePkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8"); + console.log("[colocate-standalone] ✅ standalone package.json configured with type: module"); + } + } catch (err) { + console.warn( + "[colocate-standalone] ⚠️ could not update standalone package.json:", + err.message + ); + } +} From 1fb466a1eea2597120e8c22f857be1be18968512 Mon Sep 17 00:00:00 2001 From: adevwithpurpose Date: Thu, 20 Aug 2026 23:34:08 +0500 Subject: [PATCH 096/135] fix(cli): prevent DEP0190 child process spawn deprecation on Windows (#10835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — silencia o DeprecationWarning DEP0190 do Node 22+ ao invocar wrappers .cmd/.bat no Windows via shell:true, usando windowsVerbatimArguments/windowsHide em vez da stringificação legada não segura. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - Suítes de cobertura existentes de tool-detector (cli-helper-tool-detector-paths-6162, cli-tool-detector-imports, tool-detector-win32-7279, tool-detector, tool-detector-opencode-jsonc-10227) — 23/23 passando --- src/lib/cli-helper/tool-detector.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/cli-helper/tool-detector.ts b/src/lib/cli-helper/tool-detector.ts index 8a584123ab..38a2b5d289 100644 --- a/src/lib/cli-helper/tool-detector.ts +++ b/src/lib/cli-helper/tool-detector.ts @@ -104,7 +104,8 @@ async function detectBinaryWindows( const { stdout } = await execFileImpl(located.commandPath, ["--version"], { timeout: 5000, env, - ...(useShell ? { shell: true } : {}), + windowsHide: true, + ...(useShell ? { shell: true, windowsVerbatimArguments: true } : {}), }); return { installed: true, version: stdout.trim().replace(/^v/, "") }; } catch { From 61051a146084282747d5973037f5ddf084690d01 Mon Sep 17 00:00:00 2001 From: adevwithpurpose Date: Thu, 20 Aug 2026 23:34:20 +0500 Subject: [PATCH 097/135] perf(compression): accelerate Lite and Caveman whitespace & artifact cleaners with native V8 RegExp (#10834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — ganho de performance real e bem medido: substitui loops char-a-char por RegExp nativa do V8 em cleanupArtifacts/normalizeMessageWhitespace, 53-296x mais rápido nos payloads testados (830KB: 109ms→1.6ms; 3.36MB: 296ms→5.4ms), com paridade byte-a-byte confirmada. Durante a validação do lote combinado encontramos uma regressão real: o rewrite removeu isCodeDominantText (a guarda do #9144) e seu ponto de decisão em cavemanCompress, reintroduzindo a recapitalização destrutiva de código não-cercado (function→Function). Identificado por tests/unit/compression/caveman-file-reference-9144.test.ts, que passa no tip puro do release e falhava após este PR. Restaurei a guarda em cima da nova implementação regex (commit e5edb1a6, autoria preservada + Co-authored-by), mantendo o ganho de performance sem reintroduzir o bug. Validação final (worktree combinado a partir de origin/release/v3.8.50): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/compression/lite.test.ts + caveman-*.test.ts — 99/99 passando (incluindo o #9144 restaurado) --- open-sse/services/compression/caveman.ts | 190 ++--------------------- open-sse/services/compression/lite.ts | 33 +--- 2 files changed, 11 insertions(+), 212 deletions(-) diff --git a/open-sse/services/compression/caveman.ts b/open-sse/services/compression/caveman.ts index 685d16decd..c629cb125e 100644 --- a/open-sse/services/compression/caveman.ts +++ b/open-sse/services/compression/caveman.ts @@ -204,187 +204,15 @@ export function applyRulesToText( } function cleanupArtifacts(text: string): string { - let result = text; - if (hasRepeatedHorizontalWhitespace(result)) { - result = collapseHorizontalWhitespaceRuns(result); - } - result = removeHorizontalWhitespaceBeforePunctuation(result); - result = collapseRepeatedSentencePunctuation(result); - if (result.includes(" \n") || result.includes("\t\n")) { - result = stripLineTrailingHorizontalWhitespace(result); - } - if (result.endsWith(" ") || result.endsWith("\t")) result = result.trimEnd(); - if (result.includes("\n\n\n")) result = collapseExcessNewlines(result); - if (result.startsWith("\n")) result = trimLeadingNewlines(result); - if (result.endsWith("\n")) result = trimTrailingNewlines(result); - return result; -} - -function isHorizontalWhitespace(char: string): boolean { - return char === " " || char === "\t"; -} - -function isSentencePunctuation(char: string): boolean { - return char === "." || char === "!" || char === "?"; -} - -function isCleanupPunctuation(char: string): boolean { - return ( - char === "," || char === "." || char === ";" || char === ":" || char === "!" || char === "?" - ); -} - -function hasRepeatedHorizontalWhitespace(text: string): boolean { - let previousWasWhitespace = false; - for (const char of text) { - const currentIsWhitespace = isHorizontalWhitespace(char); - if (currentIsWhitespace && previousWasWhitespace) return true; - previousWasWhitespace = currentIsWhitespace; - } - return false; -} - -function collapseHorizontalWhitespaceRuns(text: string): string { - let output = ""; - let changed = false; - - for (let index = 0; index < text.length; index++) { - const char = text[index]; - if (!isHorizontalWhitespace(char)) { - output += char; - continue; - } - - const start = index; - while (index + 1 < text.length && isHorizontalWhitespace(text[index + 1])) { - index++; - } - - if (index > start) { - output += " "; - changed = true; - } else { - output += char; - } - } - - return changed ? output : text; -} - -function removeHorizontalWhitespaceBeforePunctuation(text: string): string { - let output = ""; - let changed = false; - - for (let index = 0; index < text.length; index++) { - const char = text[index]; - if (!isHorizontalWhitespace(char)) { - output += char; - continue; - } - - const start = index; - while (index + 1 < text.length && isHorizontalWhitespace(text[index + 1])) { - index++; - } - - const nextChar = text[index + 1]; - if (nextChar && isCleanupPunctuation(nextChar)) { - changed = true; - continue; - } - - output += text.slice(start, index + 1); - } - - return changed ? output : text; -} - -function collapseRepeatedSentencePunctuation(text: string): string { - let output = ""; - let changed = false; - - for (let index = 0; index < text.length; index++) { - const char = text[index]; - if (!isSentencePunctuation(char)) { - output += char; - continue; - } - - let lastPunctuation = char; - const start = index; - while (index + 1 < text.length && isSentencePunctuation(text[index + 1])) { - index++; - lastPunctuation = text[index]; - } - - if (index > start) changed = true; - output += lastPunctuation; - } - - return changed ? output : text; -} - -function trimEndHorizontalWhitespace(text: string): string { - let end = text.length; - while (end > 0 && isHorizontalWhitespace(text[end - 1])) { - end--; - } - return end === text.length ? text : text.slice(0, end); -} - -function stripLineTrailingHorizontalWhitespace(text: string): string { - const lines = text.split("\n"); - let changed = false; - const cleanedLines = lines.map((line) => { - const cleaned = trimEndHorizontalWhitespace(line); - if (cleaned !== line) changed = true; - return cleaned; - }); - return changed ? cleanedLines.join("\n") : text; -} - -function collapseExcessNewlines(text: string): string { - let output = ""; - let changed = false; - - for (let index = 0; index < text.length; index++) { - const char = text[index]; - if (char !== "\n") { - output += char; - continue; - } - - const start = index; - while (index + 1 < text.length && text[index + 1] === "\n") { - index++; - } - - const newlineCount = index - start + 1; - if (newlineCount > 2) { - output += "\n\n"; - changed = true; - } else { - output += text.slice(start, index + 1); - } - } - - return changed ? output : text; -} - -function trimLeadingNewlines(text: string): string { - let start = 0; - while (start < text.length && text[start] === "\n") { - start++; - } - return start === 0 ? text : text.slice(start); -} - -function trimTrailingNewlines(text: string): string { - let end = text.length; - while (end > 0 && text[end - 1] === "\n") { - end--; - } - return end === text.length ? text : text.slice(0, end); + if (!text) return ""; + return text + .replace(/[ \t]{2,}/g, " ") + .replace(/[ \t]+([,.;:!?])/g, "$1") + .replace(/([.!?]){2,}/g, (m) => m[m.length - 1]) + .replace(/[ \t]+$/gm, "") + .replace(/\n{3,}/g, "\n\n") + .replace(/^\n+/, "") + .replace(/\n+$/, ""); } /** diff --git a/open-sse/services/compression/lite.ts b/open-sse/services/compression/lite.ts index 4be635da0e..ade5858352 100644 --- a/open-sse/services/compression/lite.ts +++ b/open-sse/services/compression/lite.ts @@ -20,38 +20,9 @@ interface LiteCompressionOptions { compressToolResults?: boolean; } -function trimTrailingHorizontalWhitespace(line: string): string { - let end = line.length; - while (end > 0) { - const code = line.charCodeAt(end - 1); - if (code !== 32 && code !== 9) break; - end--; - } - return end === line.length ? line : line.slice(0, end); -} - -function collapseNewlineRuns(content: string): string { - let normalized = ""; - let newlineRun = 0; - - for (const char of content) { - if (char === "\n") { - newlineRun++; - if (newlineRun <= 2) { - normalized += char; - } - continue; - } - - newlineRun = 0; - normalized += char; - } - - return normalized; -} - function normalizeMessageWhitespace(content: string): string { - return collapseNewlineRuns(content).split("\n").map(trimTrailingHorizontalWhitespace).join("\n"); + if (!content) return ""; + return content.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+$/gm, ""); } // Vision detection is centralized in `@/shared/constants/visionModels` (#4072) so From 12d0acbe06995ddc5662673d1540ce6c6bc765e7 Mon Sep 17 00:00:00 2001 From: adevwithpurpose Date: Thu, 20 Aug 2026 23:34:32 +0500 Subject: [PATCH 098/135] fix(conversations): bound reconnect walk + memoize turn hashes (#7847) (#10800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug de produção real e muito bem raiz-causado: resolveConversationId travava a request path por 10-130s em históricos longos de agente (medido em produção: p50 12.6s / max 130.2s em requests com ≥200 mensagens), por re-hashear o texto completo de cada turno a cada passo do walk de reconexão (O(starts × anchors × walkLength) HMACs síncronos). Fix cirúrgico: memoiza o hash de cada turno por request + budget de passos compartilhado entre candidatos (degrada como no-match, nunca como attach não verificado ou latência ilimitada). Resultado medido: 17.2s → 0.3s no repro. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/conversationTracker-reconnect-7847.test.ts (novo) — cobre o cap de budget, degradação com budget zero, e guarda de regressão de wall-clock (falha em 17.2s pré-fix) - 17 testes de semântica pré-existentes + conversationTurnContent (5) — passando sem alteração --- open-sse/services/conversationTracker.ts | 117 +++++++++-- ...conversationTracker-reconnect-7847.test.ts | 183 ++++++++++++++++++ 2 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 tests/unit/conversationTracker-reconnect-7847.test.ts diff --git a/open-sse/services/conversationTracker.ts b/open-sse/services/conversationTracker.ts index fadc726fc4..e24489a000 100644 --- a/open-sse/services/conversationTracker.ts +++ b/open-sse/services/conversationTracker.ts @@ -265,8 +265,24 @@ export function hashTurnContent(turn: CanonicalTurn): string { return hashHex(`${turn.role} ${turn.text}`); } +/** + * Upper bound on chain-node id computations a single resolveConversationId + * call may spend across ALL fingerprint candidates, start turns and duplicate + * anchors (#7847-class stall). Real coding-agent histories combine 1000+ + * turns with heavily duplicated tool outputs, so the (start × anchor × walk) + * product is unbounded without a cap: measured on production traffic the + * walk blocked the request path for 10-130 s before this bound existed. + * Exhausting the budget degrades exactly like a no-match — the request mints + * a new conversation — never a wrong attachment. + */ +export const DEFAULT_RECONNECT_MAX_STEPS = 150_000; + +function chainNodeIdFromHash(parentId: string, turnHash: string): string { + return hashHex(`${parentId} ${turnHash}`); +} + function chainNodeId(parentId: string, turn: CanonicalTurn): string { - return hashHex(`${parentId} ${hashTurnContent(turn)}`); + return chainNodeIdFromHash(parentId, hashTurnContent(turn)); } interface NewTurnNode { @@ -281,27 +297,28 @@ function buildNewNodes( turns: CanonicalTurn[], fromIndex: number, chainAnchor: string, - rootId: string + rootId: string, + turnHashes?: string[] ): NewTurnNode[] { const nodes: NewTurnNode[] = []; let parent = chainAnchor; for (let i = fromIndex; i < turns.length; i++) { - const turn = turns[i]; - const nodeId = chainNodeId(parent, turn); + const turnHash = turnHashes ? turnHashes[i] : hashTurnContent(turns[i]); + const nodeId = chainNodeIdFromHash(parent, turnHash); nodes.push({ id: nodeId, // The root anchor is a hashing seed, not a real node — the first turn // of a tree has no parent turn. parentId: parent === rootId ? null : parent, - role: turn.role, - contentHash: hashTurnContent(turn), + role: turns[i].role, + contentHash: turnHash, }); parent = nodeId; } return nodes; } -interface ReconnectMatch { +export interface ReconnectMatch { /** Index into `chainTurns` where the reconnection was found (turns before * this index were dropped from the chain's view — a compacted summary the * client sent instead of resending them verbatim — and are not inserted @@ -318,6 +335,30 @@ interface ReconnectMatch { anchorHasChild: boolean; } +/** + * Mutable work budget shared across a single resolveConversationId call's + * candidate walks. `stepsLeft` counts DOWN one chain-node id computation per + * step; `stepsUsed` reports total spend for observability/tests. + */ +export interface ReconnectWalkBudget { + stepsLeft: number; + stepsUsed: number; +} + +export interface FindReconnectMatchOptions { + /** Memoized `hashTurnContent` per chain turn, computed once per request. */ + turnHashes?: string[]; + /** Per-call cap; omit to use a fresh DEFAULT_RECONNECT_MAX_STEPS budget. */ + maxSteps?: number; + /** Shared budget across several calls (resolveConversationId's candidate loop). */ + budget?: ReconnectWalkBudget; +} + +export interface FindReconnectMatchResult { + match: ReconnectMatch | null; + stepsUsed: number; +} + /** * Find where `chainTurns` reconnects to an existing chain, trying the * leftmost turn first (so a still-fully-present prefix — the common case — @@ -345,21 +386,44 @@ interface ReconnectMatch { * candidate anchor for every prefix start is now tried, and the one that * verifiably extends furthest into the actual request wins — the only * reliable signal of genuine continuation when content repeats. + * + * #7847-class stall fix: the (start × anchor × walk) product over a long + * duplicate-heavy history is bounded by a step budget (`maxSteps` / + * `DEFAULT_RECONNECT_MAX_STEPS`), and turn content hashes are memoized via + * `turnHashes` so each step hashes ~130 fixed-size bytes instead of re-hashing + * the turn's full text. Budget exhaustion returns the best match verified so + * far (possibly none) — degrading to "new conversation" downstream, never an + * unverified attachment. */ -function findReconnectMatch( +export function findReconnectMatch( chainTurns: CanonicalTurn[], - index: ConversationTurnIndex -): ReconnectMatch | null { + index: ConversationTurnIndex, + options: FindReconnectMatchOptions = {} +): FindReconnectMatchResult { + const turnHashes = options.turnHashes ?? chainTurns.map(hashTurnContent); + const budget: ReconnectWalkBudget = options.budget ?? { + stepsLeft: options.maxSteps ?? DEFAULT_RECONNECT_MAX_STEPS, + stepsUsed: 0, + }; let best: ReconnectMatch | null = null; for (let s = 0; s < chainTurns.length; s++) { - const anchors = index.byContentHash.get(hashTurnContent(chainTurns[s])); + if (budget.stepsLeft <= 0) break; + const anchors = index.byContentHash.get(turnHashes[s]); if (!anchors) continue; for (const anchorNodeId of anchors) { + if (budget.stepsLeft <= 0) break; + // The anchor claim itself costs one step: with no budget left to claim + // even the hash-bucket anchor, the walker must report no match rather + // than an unverified one. + budget.stepsLeft -= 1; + budget.stepsUsed += 1; let parent = anchorNodeId; let matchEndIndex = s + 1; - for (let i = s + 1; i < chainTurns.length; i++) { - const nodeId = chainNodeId(parent, chainTurns[i]); + while (matchEndIndex < chainTurns.length && budget.stepsLeft > 0) { + budget.stepsLeft -= 1; + budget.stepsUsed += 1; + const nodeId = chainNodeIdFromHash(parent, turnHashes[matchEndIndex]); if (!index.nodeIds.has(nodeId)) break; parent = nodeId; matchEndIndex++; @@ -380,10 +444,12 @@ function findReconnectMatch( best = { startIndex: s, matchEndIndex, anchorNodeId: parent, anchorHasChild }; } // Can't do better than matching every turn through to the end. - if (matchEndIndex === chainTurns.length) return best; + if (best && best.matchEndIndex === chainTurns.length) { + return { match: best, stepsUsed: budget.stepsUsed }; + } } } - return best; + return { match: best, stepsUsed: budget.stepsUsed }; } // ── Orchestration ───────────────────────────────────────────────────────── @@ -417,13 +483,23 @@ export async function resolveConversationId( // it sits) fail to match on every request — reintroducing the exact // always-new-conversation bug this chain design exists to fix. const chainTurns = turns.filter((t) => t.role !== "system"); + // #7847-class stall fix: hash each turn's content exactly once per request + // and bound the reconnect walk across ALL candidates with one shared budget + // — previously every (start × anchor × walk-step) re-hashed the turn's full + // text twice, which on long duplicate-heavy coding-agent histories blocked + // the pre-routing request path for 10-130 s. + const turnHashes = chainTurns.map(hashTurnContent); + const walkBudget: ReconnectWalkBudget = { stepsLeft: DEFAULT_RECONNECT_MAX_STEPS, stepsUsed: 0 }; const candidates = findAgenticConversationsByFingerprint(fingerprintHash); for (const candidate of candidates) { const index = getConversationTurnIndex(candidate.id); if (index.nodeIds.size === 0) continue; - const match = findReconnectMatch(chainTurns, index); + const { match } = findReconnectMatch(chainTurns, index, { + turnHashes, + budget: walkBudget, + }); // No match anywhere in the chain means this candidate isn't actually // this conversation's lineage — it only shares the coarse fingerprint // bucket (apiKeyId/model/toolNames), which real traffic proves is not @@ -451,7 +527,8 @@ export async function resolveConversationId( chainTurns, match.matchEndIndex, match.anchorNodeId, - candidate.id + candidate.id, + turnHashes ); insertConversationTurnNodes(candidate.id, input.correlationId, newNodes); updateAgenticConversation(candidate.id, { turnCount: candidate.turnCount + 1 }); @@ -479,6 +556,10 @@ export async function resolveConversationId( const id = `conv_${randomUUID()}`; createAgenticConversation({ id, apiKeyId: input.apiKeyId, fingerprintHash }); - insertConversationTurnNodes(id, input.correlationId, buildNewNodes(chainTurns, 0, id, id)); + insertConversationTurnNodes( + id, + input.correlationId, + buildNewNodes(chainTurns, 0, id, id, turnHashes) + ); return { conversationId: id, isNewConversation: true }; } diff --git a/tests/unit/conversationTracker-reconnect-7847.test.ts b/tests/unit/conversationTracker-reconnect-7847.test.ts new file mode 100644 index 0000000000..00686f37ac --- /dev/null +++ b/tests/unit/conversationTracker-reconnect-7847.test.ts @@ -0,0 +1,183 @@ +/** + * Regression tests for the #7847-class pre-routing stall caused by the + * conversation-tracker reconnect walk + * (open-sse/services/conversationTracker.ts). + * + * findReconnectMatch evaluates every (start turn × duplicate anchor) pair and + * walks the chain forward, computing an HMAC per step. On long coding-agent + * histories (1000+ turns, heavily duplicated tool outputs) that walk is + * O(starts × anchors × walkLength) with the turn's FULL text re-hashed at + * every step — measured on production traffic as a 10-130 s synchronous + * block on the request path (chat.ts resolves the conversation id in the + * validate phase, before routing). These tests pin the two properties that + * keep it bounded: + * + * 1. The walk charges a step budget and never exceeds it (pure, no DB). + * 2. A duplicate-heavy long-history resolve completes in bounded wall time + * (DB-backed end-to-end through resolveConversationId). + */ + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { mkdtempSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-conv-7847-")); +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "conversation-7847-test-secret"; + +// Dynamic imports: modules reading DATA_DIR at top level must evaluate after +// the override above (see conversationTracker.test.ts for the full rationale). +const tracker = await import("../../open-sse/services/conversationTracker.ts"); +const { findReconnectMatch, resolveConversationId, hashTurnContent, DEFAULT_RECONNECT_MAX_STEPS } = + tracker as { + findReconnectMatch: typeof tracker.findReconnectMatch; + resolveConversationId: typeof tracker.resolveConversationId; + hashTurnContent: typeof tracker.hashTurnContent; + DEFAULT_RECONNECT_MAX_STEPS: number; + }; +const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + +test.after(() => { + try { + resetDbInstance(); + } catch { + /* DB may already be closed */ + } +}); + +function turn(role: "user" | "assistant" | "tool", text: string) { + return { role, text, blockKind: "text" as const, toolName: null }; +} + +test("findReconnectMatch is exported and enforces a step budget", () => { + assert.equal(typeof findReconnectMatch, "function", "findReconnectMatch must be exported"); + assert.ok( + DEFAULT_RECONNECT_MAX_STEPS > 0 && DEFAULT_RECONNECT_MAX_STEPS <= 500_000, + "default budget must be a sane bounded constant" + ); + + // Adversarial shape: many turns, all with the same text ("ok" tool outputs), + // and an index whose content-hash bucket holds many duplicate anchors — + // each (start, anchor) pair invites a forward walk. + const N = 400; + const chainTurns = Array.from({ length: N }, (_, i) => turn(i % 2 === 0 ? "user" : "tool", "ok")); + const okHash = hashTurnContent(turn("user", "ok")); + const anchors = Array.from({ length: 40 }, (_, i) => `anchor-${i}`); + const nodeIds = new Set(anchors); + const parentsWithChildren = new Set(); + const index = { + nodeIds, + byContentHash: new Map([[okHash, anchors]]), + parentsWithChildren, + }; + + const { stepsUsed } = findReconnectMatch(chainTurns, index, { maxSteps: 50 }); + assert.ok(stepsUsed <= 50, `budget must cap work (used ${stepsUsed})`); +}); + +test("findReconnectMatch: budget exhaustion degrades to no-match, never a wrong attach", () => { + // A genuine 3-turn continuation that WOULD match with enough budget — + // with maxSteps too small to verify even one step, the walker must return + // no match (resolveConversationId then mints a new conversation) rather + // than attaching to an unverified anchor. + const t0 = turn("user", "hello"); + const t1 = turn("assistant", "hi"); + const t2 = turn("user", "do the thing"); + const h0 = hashTurnContent(t0); + const h1 = hashTurnContent(t1); + const h2 = hashTurnContent(t2); + + // Build a real 3-node chain: n1 -> n2 -> n3. + const ids = (["", "n1", "n2", "n3"] as const).slice(0); + const chain = (parent: string, hash: string) => `node:${parent}:${hash.slice(0, 8)}`; + const n1 = chain("root", h0); + const n2 = chain(n1, h1); + const n3 = chain(n2, h2); + + const index = { + nodeIds: new Set([n1, n2, n3]), + byContentHash: new Map([ + [h0, [n1]], + [h1, [n2]], + [h2, [n3]], + ]), + parentsWithChildren: new Set([n1, n2]), + }; + void ids; + + const full = findReconnectMatch([t0, t1, t2], index); + assert.ok(full.match, "with the default budget the 3-turn continuation matches"); + assert.equal(full.match?.matchEndIndex, 3); + + const starved = findReconnectMatch([t0, t1, t2], index, { maxSteps: 0 }); + assert.equal(starved.match, null, "a zero budget must yield no match, not an unverified one"); +}); + +test("resolveConversationId: duplicate-heavy long history resolves in bounded time (#7847)", async () => { + // Shape mirrors production coding-agent traffic: ~800 turns where every + // other turn is a byte-identical short tool output (the duplicate-anchor + // amplifier the tracker's own docs describe) and the rest are large + // file-content turns. The second request edits every large turn (a + // cache-warm rewrite clients really do), so every duplicate start turn has + // hundreds of stale anchors to walk past before giving up. + const N = 800; + const pad = "x".repeat(40 * 1024); + const messages: Array> = [{ role: "system", content: "sys" }]; + for (let i = 0; i < N; i++) { + if (i % 2 === 0) { + messages.push({ role: "tool", tool_call_id: `c${i}`, content: "ok" }); + } else { + messages.push({ role: "user", content: `file ${i}\n${pad}` }); + } + } + const body1 = { model: "big-pickle-7847", messages }; + const body2 = { + model: "big-pickle-7847", + messages: [ + messages[0], + ...messages + .slice(1) + .map((m, idx) => + idx % 2 === 1 + ? { ...(m as object), content: `${(m as { content: string }).content} v2` } + : m + ), + ], + }; + + const apiKeyId = "key-7847"; + const first = await resolveConversationId({ + body: body1, + model: "big-pickle-7847", + apiKeyId, + clientSessionIdHeader: null, + correlationId: "corr-7847-1", + }); + assert.equal(first.isNewConversation, true); + + const startedAt = Date.now(); + const second = await resolveConversationId({ + body: body2, + model: "big-pickle-7847", + apiKeyId, + clientSessionIdHeader: null, + correlationId: "corr-7847-2", + }); + const elapsedMs = Date.now() - startedAt; + + // Before the bound: ~10 s+ of synchronous HMAC work on this exact shape. + // After: the walk is budget-capped and turn hashes are memoized, so the + // whole resolve stays in the tens-of-milliseconds range. 2 s leaves ample + // headroom for slow CI while still failing hard on a regression. + assert.ok( + elapsedMs < 2_000, + `duplicate-heavy resolve took ${elapsedMs}ms (budget/memoization regression)` + ); + + // Editing every large turn diverges from the recorded chain — the tracker + // must mint a new conversation for it, never attach to the stale one. + assert.equal(second.isNewConversation, true); + assert.notEqual(second.conversationId, first.conversationId); +}); From 6f28688b049037571f26ad3f56fce33e08bb1a0b Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 15:37:10 -0300 Subject: [PATCH 099/135] fix(antigravity): map Gemini 3.7 Flash tiers to upstream tiered endpoint model (#10341) (#10882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — clean single-commit cherry-pick extracted from #10879's genuinely new content (see PR body for the full extraction rationale). typecheck/file-size/changelog gates clean, 11/11 tests passing. --- .../10882-antigravity-gemini37-flash-tiers.md | 1 + open-sse/config/agyModels.ts | 9 ++++ open-sse/config/antigravityModelAliases.ts | 16 ++++++ src/shared/constants/modelSpecs.ts | 54 ++++++++++++++++--- .../agy-gemini-3696-tier-passthrough.test.ts | 5 +- tests/unit/antigravity-model-aliases.test.ts | 9 ++-- 6 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md diff --git a/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md b/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md new file mode 100644 index 0000000000..b1ff4bbf5a --- /dev/null +++ b/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md @@ -0,0 +1 @@ +- **fix(antigravity):** map Gemini 3.7 Flash tier ids (`gemini-3.7-flash-high/medium/low`, bare `gemini-3.7-flash`) to the upstream `gemini-3.7-flash-tiered` model id Google's Cloud Code endpoint expects, and configure per-tier thinking budgets ([#10882](https://github.com/diegosouzapw/OmniRoute/pull/10882)) — thanks @adevwithpurpose diff --git a/open-sse/config/agyModels.ts b/open-sse/config/agyModels.ts index 412e073044..5e9f37b84e 100644 --- a/open-sse/config/agyModels.ts +++ b/open-sse/config/agyModels.ts @@ -41,6 +41,15 @@ export const AGY_PUBLIC_MODELS = Object.freeze([ supportsVision: true, toolCalling: true, }, + { + id: "gemini-3.7-flash-tiered", + name: "Gemini 3.7 Flash (Tiered)", + contextLength: 1048576, + maxOutputTokens: 65536, + supportsReasoning: true, + supportsVision: true, + toolCalling: true, + }, // Gemini 3.1 Pro { id: "gemini-pro-agent", diff --git a/open-sse/config/antigravityModelAliases.ts b/open-sse/config/antigravityModelAliases.ts index 5eab3f4c8e..3946b776d0 100644 --- a/open-sse/config/antigravityModelAliases.ts +++ b/open-sse/config/antigravityModelAliases.ts @@ -29,6 +29,15 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([ supportsVision: true, toolCalling: true, }, + { + id: "gemini-3.7-flash-tiered", + name: "Gemini 3.7 Flash (Tiered)", + contextLength: 1048576, + maxOutputTokens: 65536, + supportsReasoning: true, + supportsVision: true, + toolCalling: true, + }, // Gemini 3.1 Pro budget tiers. Live streamGenerateContent validation uses // `gemini-pro-agent` for High; the separately advertised `gemini-3.1-pro-high` // discovery slot currently returns HTTP 400 and is intentionally not public. @@ -91,6 +100,13 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([ ]); export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({ + // Gemini 3.7 Flash tiers map to the upstream tiered endpoint model; the thinking + // budget is steered via generationConfig.thinkingConfig.thinkingBudget. + "gemini-3.7-flash": "gemini-3.7-flash-tiered", + "gemini-3.7-flash-high": "gemini-3.7-flash-tiered", + "gemini-3.7-flash-medium": "gemini-3.7-flash-tiered", + "gemini-3.7-flash-low": "gemini-3.7-flash-tiered", + "gpt-oss-120b": "gpt-oss-120b-medium", // gemini-3.1-pro-low is not aliased: the upstream accepts it verbatim. // gemini-3.1-pro-high: the discovery slot returns HTTP 400 on v1internal; // the live upstream id is gemini-pro-agent (see ANTIGRAVITY_PUBLIC_MODELS). diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index f7342c54f4..653cd2f51e 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -175,12 +175,54 @@ export const MODEL_SPECS: Record = { }, // ── Gemini 3.7 Flash (current Antigravity/AGY live tiers) ───────── - // The model id itself selects the upstream 10k/4k/1k reasoning tier. Antigravity - // still rejects client-supplied thinking parameters, so keep the explicit-parameter - // capability aligned with the existing Gemini Flash tier ids. - "gemini-3.7-flash-high": { ...GEMINI_35_FLASH_MODEL_SPEC }, - "gemini-3.7-flash-medium": { ...GEMINI_35_FLASH_MODEL_SPEC }, - "gemini-3.7-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC }, + // The tier suffix configures the thinking budget passed to the upstream + // gemini-3.7-flash-tiered backend (high: 24.5k, medium: 8k, low: 1k). + "gemini-3.7-flash-high": { + maxOutputTokens: 65536, + contextWindow: 1048576, + defaultThinkingBudget: 24576, + thinkingBudgetCap: 24576, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + }, + "gemini-3.7-flash-medium": { + maxOutputTokens: 65536, + contextWindow: 1048576, + defaultThinkingBudget: 8192, + thinkingBudgetCap: 24576, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + }, + "gemini-3.7-flash-low": { + maxOutputTokens: 65536, + contextWindow: 1048576, + defaultThinkingBudget: 1024, + thinkingBudgetCap: 24576, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + }, + "gemini-3.7-flash": { + maxOutputTokens: 65536, + contextWindow: 1048576, + defaultThinkingBudget: 8192, + thinkingBudgetCap: 24576, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + aliases: ["gemini-3.7-flash-tiered"], + }, + "gemini-3.7-flash-tiered": { + maxOutputTokens: 65536, + contextWindow: 1048576, + defaultThinkingBudget: 8192, + thinkingBudgetCap: 24576, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + }, // Provider-neutral compatibility for providers that still serve Gemini 3.6. // Antigravity/AGY availability is governed by their own provider catalogs and diff --git a/tests/unit/agy-gemini-3696-tier-passthrough.test.ts b/tests/unit/agy-gemini-3696-tier-passthrough.test.ts index 53f70b5fbc..0092681808 100644 --- a/tests/unit/agy-gemini-3696-tier-passthrough.test.ts +++ b/tests/unit/agy-gemini-3696-tier-passthrough.test.ts @@ -14,10 +14,13 @@ test("(#3696) resolveAntigravityModelId passes gemini-3.1-pro-low through unchan assert.equal(resolveAntigravityModelId("gemini-3.1-pro-low"), "gemini-3.1-pro-low"); }); -test("(#3696) no two ANTIGRAVITY_PUBLIC_MODELS entries resolve to the same upstream id", () => { +test("(#3696) non-tiered ANTIGRAVITY_PUBLIC_MODELS entries resolve to distinct upstream ids", () => { const seen = new Map(); const collisions: string[] = []; for (const model of ANTIGRAVITY_PUBLIC_MODELS) { + // Gemini 3.7 Flash tiers intentionally share the upstream `gemini-3.7-flash-tiered` + // endpoint with different reasoning token budgets. + if (model.id.startsWith("gemini-3.7-flash")) continue; const upstream = resolveAntigravityModelId(model.id); if (seen.has(upstream)) { collisions.push(`${model.id} and ${seen.get(upstream)} both resolve to "${upstream}"`); diff --git a/tests/unit/antigravity-model-aliases.test.ts b/tests/unit/antigravity-model-aliases.test.ts index 542573b3a4..82ad399215 100644 --- a/tests/unit/antigravity-model-aliases.test.ts +++ b/tests/unit/antigravity-model-aliases.test.ts @@ -57,12 +57,11 @@ test("toClientAntigravityQuotaModelId preserves upstream Gemini Flash bucket IDs test("resolveAntigravityModelId maps the documented Antigravity aliases to upstream IDs", () => { assert.equal(resolveAntigravityModelId("gemini-3-pro-image-preview"), "gemini-3-pro-image"); for (const [modelId] of EXPECTED_FLASH_TIERS) { - // Only the collapsed gemini-3.7-flash id is aliased to the live upstream - // gemini-3.7-flash-tiered id; the suffixed gemini-3.7-flash-high/medium tier ids - // (like the 3.6/3.5 tiers) have no alias entry and pass through verbatim. - const expected = modelId === "gemini-3.7-flash" ? "gemini-3.7-flash-tiered" : modelId; - assert.equal(resolveAntigravityModelId(modelId), expected); + assert.equal(resolveAntigravityModelId(modelId), "gemini-3.7-flash-tiered"); } + assert.equal(resolveAntigravityModelId("gemini-3.7-flash"), "gemini-3.7-flash-tiered"); + assert.equal(resolveAntigravityModelId("gemini-3.7-flash-tiered"), "gemini-3.7-flash-tiered"); + assert.equal(resolveAntigravityModelId("gpt-oss-120b"), "gpt-oss-120b-medium"); assert.equal(resolveAntigravityModelId("gemini-claude-sonnet-4-5"), "claude-sonnet-4-6"); assert.equal(resolveAntigravityModelId("gemini-claude-sonnet-4-5-thinking"), "claude-sonnet-4-6"); assert.equal( From 7fd82eb14613cd4b920fc11b6949f3f1a01de9cc Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:46:57 +0200 Subject: [PATCH 100/135] fix(api): guard combo updates against emptying, and fix copilot combo targets (#10866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug real e bem raiz-causado, dois bugs da mesma família: (1) PUT /api/combos/ com models:[] zerava os targets sem aviso, quebrando um combo funcionando (o invariante "combo tem ≥1 modelo" já era reforçado por ~9 consumidores downstream, menos o write path); (2) as tools do Copilot escreviam em targets em vez de models, então todo combo criado via Copilot reportava sucesso mas roteava para lugar nenhum. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/combo-empty-models.test.ts — 4/4 passando - Suíte combo completa (tests/unit/combo*.test.ts) — 1213 testes, 1208 passando, 5 falhas idênticas em ambos os lados (confirmadas DRIFT pré-existente do #9985 via probe contra o tip puro do release) --- changelog.d/fixes/10866-combo-empty-models.md | 1 + src/lib/copilot/tools.ts | 8 +-- src/shared/validation/schemas/combo.ts | 7 ++- tests/unit/combo-empty-models.test.ts | 59 +++++++++++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixes/10866-combo-empty-models.md create mode 100644 tests/unit/combo-empty-models.test.ts diff --git a/changelog.d/fixes/10866-combo-empty-models.md b/changelog.d/fixes/10866-combo-empty-models.md new file mode 100644 index 0000000000..e71092d5d4 --- /dev/null +++ b/changelog.d/fixes/10866-combo-empty-models.md @@ -0,0 +1 @@ +- fix(api): reject a combo update that removes every model, and store the copilot's combo targets where the router reads them (#10866) diff --git a/src/lib/copilot/tools.ts b/src/lib/copilot/tools.ts index a4bc59321c..095f7cf4cd 100644 --- a/src/lib/copilot/tools.ts +++ b/src/lib/copilot/tools.ts @@ -121,11 +121,7 @@ export const COPILOT_TOOLS: CopilotTool[] = [ let output = `**${combos.length} combo(s) configured**\n\n`; for (const c of combos as any[]) { const active = c.isActive ? "✅" : "⛔"; - const targets = c.targets - ? typeof c.targets === "string" - ? JSON.parse(c.targets).length - : c.targets.length - : 0; + const targets = Array.isArray(c.models) ? c.models.length : 0; output += `${active} **${c.name}** — strategy: \`${c.strategy}\` — ${targets} target(s)\n`; } return output; @@ -165,7 +161,7 @@ export const COPILOT_TOOLS: CopilotTool[] = [ const combo = await createCombo({ name, strategy, - targets: JSON.stringify(targets), + models: targets, isActive: true, }); const anyCombo = combo as any; diff --git a/src/shared/validation/schemas/combo.ts b/src/shared/validation/schemas/combo.ts index f9b7e3c832..79bb9ca56c 100644 --- a/src/shared/validation/schemas/combo.ts +++ b/src/shared/validation/schemas/combo.ts @@ -379,7 +379,12 @@ export const updateComboSchema = z .object({ name: comboNameSchema.optional(), description: z.string().max(2000).optional().nullable(), - models: z.array(comboModelEntry).optional(), + // Creation may leave `models` empty (`omniroute combo create` drafts one + // that way); an update may not, or a working combo loses every target. + models: z + .array(comboModelEntry) + .min(1, "an update cannot remove every model from a combo") + .optional(), strategy: comboStrategySchema.optional(), config: comboRuntimeConfigSchema.optional(), isActive: z.boolean().optional(), diff --git a/tests/unit/combo-empty-models.test.ts b/tests/unit/combo-empty-models.test.ts new file mode 100644 index 0000000000..1486628ec5 --- /dev/null +++ b/tests/unit/combo-empty-models.test.ts @@ -0,0 +1,59 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import assert from "node:assert/strict"; +import test from "node:test"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-empty-models-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { createComboSchema, updateComboSchema } = + await import("../../src/shared/validation/schemas/combo.ts"); +const { getCopilotTool } = await import("../../src/lib/copilot/tools.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const core = await import("../../src/lib/db/core.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("an update cannot remove every model from a combo", () => { + assert.equal(updateComboSchema.safeParse({ models: [] }).success, false); + assert.equal(updateComboSchema.safeParse({ models: ["openai/gpt-4o-mini"] }).success, true); + assert.equal(updateComboSchema.safeParse({ name: "renamed" }).success, true); +}); + +test("creating a combo with no model stays allowed — the CLI does it on purpose", () => { + assert.equal(createComboSchema.safeParse({ name: "drafted", models: [] }).success, true); + assert.equal(createComboSchema.safeParse({ name: "drafted" }).success, true); +}); + +test("the copilot createCombo tool stores targets where the router looks for them", async () => { + const tool = getCopilotTool("createCombo"); + assert.ok(tool); + + await tool.handler({ + name: "copilot-stored", + strategy: "priority", + targets: JSON.stringify([{ provider: "openai", model: "gpt-4o-mini", weight: 100 }]), + }); + + const stored = (await combosDb.getComboByName("copilot-stored")) as { models?: unknown[] } | null; + assert.ok(stored, "the combo should exist"); + assert.equal(stored.models?.length, 1); +}); + +test("the copilot combo list counts the targets the router will use", async () => { + const list = getCopilotTool("listCombos"); + assert.ok(list); + + await combosDb.createCombo({ + name: "dashboard-made", + strategy: "priority", + models: ["openai/gpt-4o-mini", "openai/gpt-4o"], + }); + + const output = await list.handler({}); + assert.match(output, /\*\*dashboard-made\*\* — strategy: `priority` — 2 target\(s\)/); +}); From 362c5acbfe025c190dcd91700b1c114f6b5131f2 Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:47:05 +0200 Subject: [PATCH 101/135] feat(api): accept PATCH on /api/combos/[id] (#10869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug real de contrato: o openapi.yaml já documentava patch em /api/combos/{id}, mas a rota nunca exportou PATCH, então um cliente gerado a partir do spec publicado recebia 405 do App Router antes de qualquer handler rodar. Fix mínimo (delegação de 4 linhas para PUT, mesmo padrão já usado em /api/providers/[id] e 25 outras rotas /api/**). Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/combo-patch-verb.test.ts — 2/2 passando (falham antes com "comboRoute.PATCH is not a function") - Suíte combo completa — mesmas 5 falhas herdadas de #9985, confirmadas DRIFT --- .../features/10869-combo-patch-verb.md | 1 + src/app/api/combos/[id]/route.ts | 6 ++ tests/unit/combo-patch-verb.test.ts | 58 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 changelog.d/features/10869-combo-patch-verb.md create mode 100644 tests/unit/combo-patch-verb.test.ts diff --git a/changelog.d/features/10869-combo-patch-verb.md b/changelog.d/features/10869-combo-patch-verb.md new file mode 100644 index 0000000000..f11893d95c --- /dev/null +++ b/changelog.d/features/10869-combo-patch-verb.md @@ -0,0 +1 @@ +- feat(api): accept PATCH on /api/combos/[id], the verb the OpenAPI spec already documents (#10869) diff --git a/src/app/api/combos/[id]/route.ts b/src/app/api/combos/[id]/route.ts index 869114486f..dd35562bdf 100644 --- a/src/app/api/combos/[id]/route.ts +++ b/src/app/api/combos/[id]/route.ts @@ -265,6 +265,12 @@ export async function PUT(request, { params }) { } } +// PATCH /api/combos/[id] - partial update. PUT merges the body onto the stored +// combo, so both verbs share one handler (same shape as /api/providers/[id]). +export async function PATCH(request, ctx) { + return PUT(request, ctx); +} + // DELETE /api/combos/[id] - Delete combo export async function DELETE(request, { params }) { const authError = await requireManagementAuth(request); diff --git a/tests/unit/combo-patch-verb.test.ts b/tests/unit/combo-patch-verb.test.ts new file mode 100644 index 0000000000..c72d0e23d4 --- /dev/null +++ b/tests/unit/combo-patch-verb.test.ts @@ -0,0 +1,58 @@ +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-combo-patch-verb-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const comboRoute = await import("../../src/app/api/combos/[id]/route.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +function patch(id: string, body: Record) { + return new Request(`http://localhost/api/combos/${id}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +test("PATCH changes one field and leaves the rest of the combo alone", async () => { + const combo = await combosDb.createCombo({ + name: "patchable", + strategy: "priority", + models: [{ provider: "openai", model: "gpt-4o" }], + system_message: "keep me", + }); + + const response = await comboRoute.PATCH(patch(combo.id, { strategy: "round-robin" }), { + params: Promise.resolve({ id: combo.id }), + }); + assert.equal(response.status, 200); + + const stored = (await combosDb.getComboById(combo.id)) as { + strategy?: string; + system_message?: string; + models?: unknown[]; + }; + assert.equal(stored.strategy, "round-robin"); + assert.equal(stored.system_message, "keep me"); + assert.equal(stored.models?.length, 1); +}); + +test("PATCH on an unknown combo answers 404, like PUT", async () => { + const response = await comboRoute.PATCH(patch("does-not-exist", { strategy: "priority" }), { + params: Promise.resolve({ id: "does-not-exist" }), + }); + assert.equal(response.status, 404); + + const body = (await response.json()) as { error?: { code?: string } }; + assert.equal(body.error?.code, "COMBO_007"); +}); From dacf4c3c1af6b11305f0d6683acf47d3c2abb56a Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:47:15 +0200 Subject: [PATCH 102/135] fix(cli): report .env lines that never take effect (#10870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — resolve o cenário real do #6194: uma linha de .env que o shell já tinha exportado antes (ex.: HOSTNAME=0.0.0.0) era silenciosamente ignorada pelo loader first-wins, sem nenhum aviso — o servidor bindava no hostname da máquina, localhost parava de responder, e ModelSync/health checks falhavam com ECONNREFUSED sem pista nenhuma. Agora cada chave mascarada emite um warning em stderr (nome da chave + as duas origens, nunca o valor); um .env ilegível também vira warning em vez de falha silenciosa no boot. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/cli-env-collision.test.ts — 4/4 passando (3 falham no base) - Suítes CLI env vizinhas (cli-data-dir-env-loading, cli-env-inline-comment-10100, cli-data-dir-env, cli-entrypoint, cli-electron-to-cli-migration-server-env-7302, cli-storage-key-bootstrap) — intactas e verdes --- bin/omniroute.mjs | 19 ++- changelog.d/fixes/10870-cli-env-collision.md | 1 + tests/unit/cli-env-collision.test.ts | 130 +++++++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10870-cli-env-collision.md create mode 100644 tests/unit/cli-env-collision.test.ts diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index c023879da8..fb0a455520 100755 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -119,6 +119,9 @@ function loadEnvFile() { addEnvPath(join(ROOT, ".env")); } + const keyOrigin = new Map(); + const shadowed = new Map(); + for (const envPath of envPaths) { try { if (existsSync(envPath)) { @@ -131,19 +134,31 @@ function loadEnvFile() { const key = trimmed.slice(0, eqIdx).trim(); if (process.env[key] === undefined) { process.env[key] = parseEnvValue(trimmed.slice(eqIdx + 1)); + keyOrigin.set(key, envPath); + } else if (!shadowed.has(key)) { + // The line is inert: something set this key first. Report it once + // per key, whether the winner was an earlier file or the process + // environment (#6194: a shell's own HOSTNAME beat the .env and the + // server bound to the wrong address in silence). + shadowed.set(key, { winner: keyOrigin.get(key) ?? null, loser: envPath }); } } } loadedEnvPaths.push(envPath); } - } catch { - // Ignore errors reading env files. + } catch (err) { + console.warn(` \x1b[33m⚠ Could not read ${envPath}: ${err?.message ?? err}\x1b[0m`); } } for (const envPath of loadedEnvPaths) { console.log(` \x1b[2m📋 Loaded env from ${envPath}\x1b[0m`); } + + for (const [key, { winner, loser }] of shadowed) { + const setter = winner ? winner : "the environment"; + console.warn(` \x1b[33m⚠ ${key} in ${loser} is ignored, ${setter} set it first\x1b[0m`); + } } loadEnvFile(); diff --git a/changelog.d/fixes/10870-cli-env-collision.md b/changelog.d/fixes/10870-cli-env-collision.md new file mode 100644 index 0000000000..95a428ba08 --- /dev/null +++ b/changelog.d/fixes/10870-cli-env-collision.md @@ -0,0 +1 @@ +- fix(cli): warn when a .env line never takes effect, and stop swallowing an unreadable .env (#10870) diff --git a/tests/unit/cli-env-collision.test.ts b/tests/unit/cli-env-collision.test.ts new file mode 100644 index 0000000000..cb11ac4474 --- /dev/null +++ b/tests/unit/cli-env-collision.test.ts @@ -0,0 +1,130 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); +const BIN = path.join(ROOT, "bin", "omniroute.mjs"); + +function layout() { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cli-env-collision-")); + const home = path.join(tmp, "home"); + const dataDir = path.join(tmp, "data"); + const cwd = path.join(tmp, "cwd"); + const appDataDir = + process.platform === "win32" + ? path.join(tmp, "appdata", "omniroute") + : path.join(home, ".omniroute"); + fs.mkdirSync(dataDir, { recursive: true }); + fs.mkdirSync(appDataDir, { recursive: true }); + fs.mkdirSync(cwd, { recursive: true }); + return { tmp, home, dataDir, cwd }; +} + +function runCli( + { tmp, home, dataDir, cwd }: ReturnType, + extraEnv: Record = {} +) { + const cleanEnv = { ...process.env }; + for (const key of ["OMNIROUTE_BASE_URL", "PORT", "STORAGE_ENCRYPTION_KEY"]) { + delete cleanEnv[key]; + } + return spawnSync("node", [BIN, "env", "show", "--json"], { + cwd, + env: { + ...cleanEnv, + DATA_DIR: dataDir, + HOME: home, + USERPROFILE: home, + APPDATA: path.join(tmp, "appdata"), + CI: "1", + OMNIROUTE_CLI_SKIP_REPO_ENV: "1", + OMNIROUTE_NO_UPDATE_NOTIFIER: "1", + ...extraEnv, + }, + encoding: "utf-8", + timeout: 60_000, + }); +} + +test("a key masked by an earlier .env is named, with both files and without its value", () => { + const dirs = layout(); + try { + fs.writeFileSync( + path.join(dirs.dataDir, ".env"), + "OMNIROUTE_BASE_URL=https://data.example/v1\n" + ); + fs.writeFileSync(path.join(dirs.cwd, ".env"), "OMNIROUTE_BASE_URL=https://cwd.example/v1\n"); + + const stderr = runCli(dirs).stderr ?? ""; + + assert.match(stderr, /OMNIROUTE_BASE_URL/); + assert.ok(stderr.includes(path.join(dirs.cwd, ".env")), `ignored file named: ${stderr}`); + assert.ok(stderr.includes(path.join(dirs.dataDir, ".env")), `winning file named: ${stderr}`); + assert.ok(!stderr.includes("cwd.example"), "the ignored value must never be printed"); + assert.ok(!stderr.includes("data.example"), "the winning value must never be printed"); + } finally { + fs.rmSync(dirs.tmp, { recursive: true, force: true }); + } +}); + +test("a key each file declares once says nothing", () => { + const dirs = layout(); + try { + fs.writeFileSync( + path.join(dirs.dataDir, ".env"), + "OMNIROUTE_BASE_URL=https://data.example/v1\n" + ); + fs.writeFileSync(path.join(dirs.cwd, ".env"), "PORT=34567\n"); + + const stderr = runCli(dirs).stderr ?? ""; + assert.ok(!/OMNIROUTE_BASE_URL|PORT/.test(stderr), `nothing to report: ${stderr}`); + } finally { + fs.rmSync(dirs.tmp, { recursive: true, force: true }); + } +}); + +test("a key the environment already set is reported too — that is #6194", () => { + const dirs = layout(); + try { + fs.writeFileSync( + path.join(dirs.dataDir, ".env"), + "OMNIROUTE_BASE_URL=https://data.example/v1\n" + ); + + const stderr = runCli(dirs, { OMNIROUTE_BASE_URL: "https://shell.example/v1" }).stderr ?? ""; + + assert.match(stderr, /OMNIROUTE_BASE_URL/); + assert.ok(stderr.includes(path.join(dirs.dataDir, ".env")), `inert file named: ${stderr}`); + assert.match(stderr, /environment/); + assert.ok(!stderr.includes("shell.example"), "the winning value must never be printed"); + assert.ok(!stderr.includes("data.example"), "the ignored value must never be printed"); + } finally { + fs.rmSync(dirs.tmp, { recursive: true, force: true }); + } +}); + +test("an unreadable .env is reported instead of being swallowed", () => { + const dirs = layout(); + try { + // A directory named `.env` passes existsSync and makes readFileSync throw + // EISDIR for any user, root included — unlike chmod 000. + fs.mkdirSync(path.join(dirs.cwd, ".env"), { recursive: true }); + fs.writeFileSync( + path.join(dirs.dataDir, ".env"), + "OMNIROUTE_BASE_URL=https://data.example/v1\n" + ); + + const result = runCli(dirs); + assert.equal(result.status, 0, "an unreadable .env must stay non-fatal"); + assert.ok( + (result.stderr ?? "").includes(path.join(dirs.cwd, ".env")), + `the unreadable file should be named: ${result.stderr}` + ); + } finally { + fs.rmSync(dirs.tmp, { recursive: true, force: true }); + } +}); From 82ed31d27a2df8d8cd4da300c786f48e7d465216 Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:47:24 +0200 Subject: [PATCH 103/135] docs(openapi): document GET and PUT on /api/combos/[id] (#10875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — TDD exemplar num gap real de contrato: as duas operações que o dashboard realmente chama em /api/combos/{id} (GET e PUT) estavam ausentes do openapi.yaml, enquanto a única operação documentada (patch, antes deste #10869) não tinha handler. Adiciona um floor de cobertura por OPERAÇÃO (não só por PATH) que o gate existente não capturava, medido em 343/985 (34.8%). Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/openapi-coverage.test.ts — passando com o novo floor de operações - openapi-routes/openapi-coverage/openapi-security-tiers gates — PASS --- .../10875-combos-id-verb-coverage.md | 1 + docs/openapi.yaml | 33 ++++++++ tests/unit/openapi-coverage.test.ts | 77 +++++++++++++++++-- 3 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 changelog.d/maintenance/10875-combos-id-verb-coverage.md diff --git a/changelog.d/maintenance/10875-combos-id-verb-coverage.md b/changelog.d/maintenance/10875-combos-id-verb-coverage.md new file mode 100644 index 0000000000..5610a7ea41 --- /dev/null +++ b/changelog.d/maintenance/10875-combos-id-verb-coverage.md @@ -0,0 +1 @@ +- **docs(openapi):** document the `GET` and `PUT` operations on `/api/combos/{id}`, and add an operation-level coverage floor so a missing verb can no longer hide behind a path that already counts as covered ([#10875](https://github.com/diegosouzapw/OmniRoute/pull/10875)) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 54689a0a57..fb255d3543 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -2069,6 +2069,39 @@ paths: description: Created combo /api/combos/{id}: + get: + tags: [Combos] + summary: Get combo by ID + parameters: + - $ref: "#/components/parameters/ResourceId" + responses: + "200": + description: Combo details + "404": + description: Combo not found + put: + tags: [Combos] + summary: Update combo + description: >- + Partial update: the body is merged onto the stored combo, so a field left out keeps + its current value. An array that IS sent replaces the stored one outright. + parameters: + - $ref: "#/components/parameters/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "200": + description: Updated combo + "400": + description: Invalid body, or the resulting combo fails validation + "404": + description: Combo not found + "409": + description: Name already taken, or the combo is quota-share managed patch: tags: [Combos] summary: Update combo diff --git a/tests/unit/openapi-coverage.test.ts b/tests/unit/openapi-coverage.test.ts index c07b545efc..c762024ec3 100644 --- a/tests/unit/openapi-coverage.test.ts +++ b/tests/unit/openapi-coverage.test.ts @@ -8,13 +8,13 @@ const ROOT = process.cwd(); const API_ROOT = path.join(ROOT, "src", "app", "api"); const OPENAPI_PATH = path.join(ROOT, "docs", "openapi.yaml"); -function collectRoutePaths(dir: string): string[] { +function collectRouteFiles(dir: string): { apiPath: string; file: string }[] { const entries = fs.readdirSync(dir, { withFileTypes: true }); - const paths: string[] = []; + const routes: { apiPath: string; file: string }[] = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { - paths.push(...collectRoutePaths(fullPath)); + routes.push(...collectRouteFiles(fullPath)); continue; } if (entry.isFile() && entry.name === "route.ts") { @@ -22,10 +22,31 @@ function collectRoutePaths(dir: string): string[] { .dirname(fullPath) .replace(API_ROOT, "") .replace(/\[([^\]]+)\]/g, "{$1}"); - paths.push(`/api${apiPath}`); + routes.push({ apiPath: `/api${apiPath}`, file: fullPath }); } } - return paths; + return routes; +} + +function collectRoutePaths(dir: string): string[] { + return collectRouteFiles(dir).map((route) => route.apiPath); +} + +// OPTIONS is deliberately absent: every v1 route exports it for CORS preflight, so it is +// transport boilerplate rather than API surface a consumer calls. +const DOCUMENTABLE_METHODS = ["get", "post", "put", "patch", "delete", "head"] as const; + +/** The HTTP handlers a route.ts actually exports, across the export forms used in this repo. */ +function exportedMethods(routeFile: string): string[] { + const source = fs.readFileSync(routeFile, "utf-8"); + return DOCUMENTABLE_METHODS.filter((method) => { + const name = method.toUpperCase(); + return ( + new RegExp(`export\\s+(?:async\\s+)?function\\s+${name}\\b`).test(source) || + new RegExp(`export\\s+(?:const|let|var)\\s+${name}\\b`).test(source) || + new RegExp(`export\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`).test(source) + ); + }); } function normalizePath(p: string): string { @@ -79,3 +100,49 @@ test("openapi.yaml does not regress documented-route coverage below the agreed f `Missing: ${missing.slice(0, 10).join(", ")}${missing.length > 10 ? ` ... +${missing.length - 10} more` : ""}` ); }); + +// Floor recorded on 2026-08-20 for release/v3.8.50: 343/985 operations documented. +// The path floor above cannot see an operation: a route counts as covered the moment ONE +// of its verbs is documented. /api/combos/{id} exported GET, PUT and DELETE while the spec +// listed only `patch` and `delete` — a fully covered path hiding two operations, and the +// one `patch` it did document does not exist on that route. Schemathesis (dast-smoke.yml) +// only exercises documented operations, so the two hidden verbs never reached the fuzzer. +// Same "no regressions, not the absolute target" policy as the path floor: raising it is +// tracked as the same follow-up doc debt. +const OPENAPI_OPERATION_FLOOR_PERCENT = 34.8; + +test("openapi.yaml does not regress documented-operation coverage below the agreed floor", () => { + const raw = yaml.load(fs.readFileSync(OPENAPI_PATH, "utf-8")) as { + paths?: Record>; + }; + const documentedPaths = raw.paths ?? {}; + + let covered = 0; + const missing: string[] = []; + + for (const { apiPath, file } of collectRouteFiles(API_ROOT)) { + const operations = documentedPaths[normalizePath(apiPath)]; + for (const method of exportedMethods(file)) { + if (operations && operations[method]) { + covered++; + } else { + missing.push(`${method.toUpperCase()} ${apiPath}`); + } + } + } + + const total = covered + missing.length; + const coverage = (covered / total) * 100; + + if (coverage < OPENAPI_OPERATION_FLOOR_PERCENT) { + console.error(`Operation coverage: ${coverage.toFixed(1)}% (${covered}/${total})`); + console.error("Undocumented operations:"); + missing.forEach((op) => console.error(` - ${op}`)); + } + + assert.ok( + coverage >= OPENAPI_OPERATION_FLOOR_PERCENT, + `OpenAPI operation coverage regressed: ${coverage.toFixed(1)}% < floor ${OPENAPI_OPERATION_FLOOR_PERCENT}%. ` + + `Undocumented: ${missing.slice(0, 10).join(", ")}${missing.length > 10 ? ` ... +${missing.length - 10} more` : ""}` + ); +}); From e6801bace1b63fab76c153e54480b986b4a27428 Mon Sep 17 00:00:00 2001 From: Dizzle <112548150+maxmad64bis@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:47:34 +0200 Subject: [PATCH 104/135] feat(proxy): surface anonymous egress-IP sharing in the health sweep and the egress API (#10876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — expõe onde o operador realmente olha (sweep periódico de saúde de proxy + resposta da API de egress) o sinal de compartilhamento anônimo de IP de egress entre contas de um mesmo rotation group, que já existia (analyzeEgressSharing) mas só era acessível via curl autenticado. Fecha #10677. Respeita a decisão de redação do #10348/#10539: apenas contagens por padrão, nenhum IP/identidade de conta a menos que PROXY_LOG_INCLUDE_IPS=true. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/proxy-egress-route-summary.test.ts + proxy-egress-summary.test.ts + proxy-health-egress-line.test.ts — 23/23 passando (agregado, formatter, linha do sweep com output real capturado, rota completa com auth de management) --- .../features/10677-egress-sharing-summary.md | 1 + src/app/api/settings/proxies/egress/route.ts | 13 +- src/lib/proxyEgress.ts | 104 +++++++++++++++ src/lib/proxyHealth/scheduler.ts | 41 ++++++ src/lib/proxyLogger.ts | 13 +- tests/unit/proxy-egress-route-summary.test.ts | 83 ++++++++++++ tests/unit/proxy-egress-summary.test.ts | 118 +++++++++++++++++ tests/unit/proxy-health-egress-line.test.ts | 119 ++++++++++++++++++ 8 files changed, 485 insertions(+), 7 deletions(-) create mode 100644 changelog.d/features/10677-egress-sharing-summary.md create mode 100644 tests/unit/proxy-egress-route-summary.test.ts create mode 100644 tests/unit/proxy-egress-summary.test.ts create mode 100644 tests/unit/proxy-health-egress-line.test.ts diff --git a/changelog.d/features/10677-egress-sharing-summary.md b/changelog.d/features/10677-egress-sharing-summary.md new file mode 100644 index 0000000000..9e1f723a0d --- /dev/null +++ b/changelog.d/features/10677-egress-sharing-summary.md @@ -0,0 +1 @@ +- **feat(proxy):** the proxy-health sweep and `GET /api/settings/proxies/egress` now report an anonymous summary of egress-IP sharing — how many rotation groups share an egress IP and the largest number of accounts behind one IP — computed from persisted `proxy_logs` over a 24h window. No IPs and no account identities by default; `PROXY_LOG_INCLUDE_IPS=true` restores raw details. ([#10677](https://github.com/diegosouzapw/OmniRoute/issues/10677)) diff --git a/src/app/api/settings/proxies/egress/route.ts b/src/app/api/settings/proxies/egress/route.ts index b43fcd6e6f..dbad250361 100644 --- a/src/app/api/settings/proxies/egress/route.ts +++ b/src/app/api/settings/proxies/egress/route.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; -import { diagnoseAllEgressIps, validateProxyPool } from "@/lib/proxyEgress"; +import { + diagnoseAllEgressIps, + getRecentEgressSharingSummary, + validateProxyPool, +} from "@/lib/proxyEgress"; /** * GET /api/settings/proxies/egress — diagnose the egress IP of every OAuth @@ -17,8 +21,11 @@ export async function GET(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; try { - const diagnostic = await diagnoseAllEgressIps(); - return NextResponse.json(diagnostic); + const [diagnostic, { summary }] = await Promise.all([ + diagnoseAllEgressIps(), + getRecentEgressSharingSummary(), + ]); + return NextResponse.json({ ...diagnostic, summary }); } catch (error) { return createErrorResponseFromUnknown(error, "Failed to diagnose egress IPs"); } diff --git a/src/lib/proxyEgress.ts b/src/lib/proxyEgress.ts index bcad4e2d7b..1996bec763 100644 --- a/src/lib/proxyEgress.ts +++ b/src/lib/proxyEgress.ts @@ -196,6 +196,110 @@ export function analyzeEgressSharing(connections: ConnectionEgress[]): { return { byEgressIp, sharedWithinRotationGroup }; } +export const EGRESS_SHARING_WINDOW_MS = 24 * 60 * 60 * 1000; + +export interface EgressLogRow { + provider: string | null; + account: string | null; + connectionId: string | null; + egressIp: string | null; +} + +export interface EgressSharingSummary { + windowStart: string; + windowEnd: string; + distinctEgressIps: number; + sharingByRotationGroup: Array<{ + rotationGroup: string; + sharedIps: number; + maxAccountsSharingOneIp: number; + }>; + maxAccountsSharingOneIp: number; +} + +/** + * PURE: anonymous egress-IP sharing summary over proxy_logs-shaped rows. + * Dedupes per connection (proxy_logs holds one row per request, not per + * connection) — "max accounts behind one IP" therefore counts connections, + * not distinct accounts, when one account spans several connections. Reuses + * analyzeEgressSharing's rotation-group semantics and returns counts only — + * no IP literals, no account identities (#10348). + */ +export function summarizeEgressSharing( + rows: EgressLogRow[], + window: { start: string; end: string } +): { summary: EgressSharingSummary; warnings: EgressSharingWarning[] } { + const byAccount = new Map(); + for (const r of rows) { + if (!r.egressIp) continue; + const key = r.connectionId ?? r.account; + if (!key) continue; + if (!byAccount.has(key)) byAccount.set(key, r); + } + + const connections = [...byAccount.values()].map((r) => ({ + connectionId: r.connectionId ?? r.account ?? "unknown", + provider: r.provider ?? "", + account: r.account ?? r.connectionId, + proxyLevel: "log", + proxyHost: null, + egressIp: r.egressIp, + })); + + const { byEgressIp, sharedWithinRotationGroup } = analyzeEgressSharing(connections); + + const byGroup = new Map(); + let maxAccountsSharingOneIp = 0; + for (const w of sharedWithinRotationGroup) { + const g = byGroup.get(w.rotationGroup) ?? { sharedIps: 0, maxAccountsSharingOneIp: 0 }; + g.sharedIps++; + g.maxAccountsSharingOneIp = Math.max(g.maxAccountsSharingOneIp, w.connections.length); + byGroup.set(w.rotationGroup, g); + maxAccountsSharingOneIp = Math.max(maxAccountsSharingOneIp, w.connections.length); + } + + return { + summary: { + windowStart: window.start, + windowEnd: window.end, + distinctEgressIps: Object.keys(byEgressIp).length, + sharingByRotationGroup: [...byGroup.entries()].map(([rotationGroup, v]) => ({ + rotationGroup, + sharedIps: v.sharedIps, + maxAccountsSharingOneIp: v.maxAccountsSharingOneIp, + })), + maxAccountsSharingOneIp, + }, + // Raw warnings carry IPs and labels — only ever rendered behind the + // PROXY_LOG_INCLUDE_IPS opt-in (#10348), never in the summary itself. + warnings: sharedWithinRotationGroup, + }; +} + +/** + * DB-backed: anonymous egress-sharing summary over the last + * EGRESS_SHARING_WINDOW_MS of persisted proxy_logs (egress_ip is always + * persisted even when the process log line is redacted). No live probes. + * Single place where proxy_logs rows are mapped to EgressLogRow — the sweep + * and the route both consume this helper. Reads all rows in the window (the + * existing SELECT * has no LIMIT); bounded by the 24h window. + */ +export async function getRecentEgressSharingSummary(): Promise<{ + summary: EgressSharingSummary; + warnings: EgressSharingWarning[]; +}> { + const { exportProxyLogsSince } = await import("./db/proxyLogs"); + const end = new Date(); + const start = new Date(end.getTime() - EGRESS_SHARING_WINDOW_MS); + const rows: EgressLogRow[] = exportProxyLogsSince(start.toISOString()).map((r) => ({ + provider: (r.provider as string | null) ?? null, + account: (r.account as string | null) ?? null, + connectionId: (r.connection_id as string | null) ?? null, + egressIp: (r.egress_ip as string | null) ?? null, + })); + return summarizeEgressSharing(rows, { start: start.toISOString(), end: end.toISOString() }); +} + /** * Diagnose egress IPs for every OAuth connection: resolve each connection's * proxy, probe the real egress IP, and flag same-rotation-group IP sharing. diff --git a/src/lib/proxyHealth/scheduler.ts b/src/lib/proxyHealth/scheduler.ts index 7e7c24f984..dc30453a34 100644 --- a/src/lib/proxyHealth/scheduler.ts +++ b/src/lib/proxyHealth/scheduler.ts @@ -23,6 +23,12 @@ */ import { deleteProxyById, listProxies, updateProxy } from "@/lib/localDb"; +import { isProxyLogIncludeIps } from "@/lib/proxyLogger"; +import { + getRecentEgressSharingSummary, + type EgressSharingSummary, + type EgressSharingWarning, +} from "@/lib/proxyEgress"; import { createProxyDispatcher, clearDispatcherCache, @@ -70,6 +76,26 @@ function getFailureMap(): Map { return globalThis.__proxyHealthConsecutiveFailures; } +/** + * PURE: one-line anonymous egress-sharing summary for the sweep log (#10677). + * Counts only by default; raw shared IPs only when PROXY_LOG_INCLUDE_IPS=true + * (the redaction decision from #10348 — never leak IPs or account labels). + */ +export function formatEgressSharingSummaryLine( + summary: EgressSharingSummary, + warnings: EgressSharingWarning[], + includeDetails: boolean +): string { + const base = + `${LOG_PREFIX} egress: ${summary.sharingByRotationGroup.length} rotation group(s) share an ` + + `egress IP (max ${summary.maxAccountsSharingOneIp} accounts)`; + if (!includeDetails) return base; + const detail = warnings + .map((w) => `${w.rotationGroup}: ${w.egressIp} (${w.connections.length} accounts)`) + .join(", "); + return detail ? `${base} — ${detail}` : base; +} + function isEnabled(): boolean { return process.env.PROXY_HEALTH_ENABLED !== "false"; } @@ -162,6 +188,21 @@ async function testOneProxy(proxy: { } async function sweep(): Promise { + // #10677: anonymous egress-sharing signal from persisted proxy_logs (no live + // probes). Logged only when sharing exists — the sweep line is a warning + // signal, not a heartbeat. Runs before the empty-registry early return so + // sharing from direct connections is still reported when no proxies are + // configured. Never let a DB hiccup suppress the completion line or fail the + // sweep itself. + try { + const { summary, warnings } = await getRecentEgressSharingSummary(); + if (summary.sharingByRotationGroup.length > 0) { + console.log(formatEgressSharingSummaryLine(summary, warnings, isProxyLogIncludeIps())); + } + } catch (error) { + console.error(`${LOG_PREFIX} Egress summary skipped:`, error); + } + const { items: proxies } = await listProxies({ includeSecrets: true }); if (proxies.length === 0) return; diff --git a/src/lib/proxyLogger.ts b/src/lib/proxyLogger.ts index 79bb4e5af4..a9eb4b3805 100644 --- a/src/lib/proxyLogger.ts +++ b/src/lib/proxyLogger.ts @@ -110,9 +110,14 @@ loadFromDb(); // neither IPs nor the account prefix. Deliberately NOT coupled to debugMode // (src/lib/db/settings.ts defaults debugMode to true) — this verbosity is opt-in only. // Storage (in-memory ring buffer + SQLite) is untouched and always keeps full IPs. -const PROXY_LOG_INCLUDE_IPS = - process.env.PROXY_LOG_INCLUDE_IPS === "true" || - process.env.PROXY_LOG_INCLUDE_IPS === "1"; + +/** Read at call time so tests can toggle it between imports. */ +export function isProxyLogIncludeIps(): boolean { + return ( + process.env.PROXY_LOG_INCLUDE_IPS === "true" || + process.env.PROXY_LOG_INCLUDE_IPS === "1" + ); +} /** * Pure formatter for the [ProxyEgress] process-log line (#10348). At the default level it @@ -178,7 +183,7 @@ export function logProxyEvent(entry: ProxyLogInput) { level: log.level, proxyHost: log.proxy?.host, status: log.status, - includeDetails: PROXY_LOG_INCLUDE_IPS, + includeDetails: isProxyLogIncludeIps(), }) ); } diff --git a/tests/unit/proxy-egress-route-summary.test.ts b/tests/unit/proxy-egress-route-summary.test.ts new file mode 100644 index 0000000000..bd5bc45b9a --- /dev/null +++ b/tests/unit/proxy-egress-route-summary.test.ts @@ -0,0 +1,83 @@ +/** + * GET /api/settings/proxies/egress returns the existing + * diagnostic payload PLUS an additive anonymous `summary` computed from + * persisted proxy_logs. Auth pattern from api-auth.test.ts; probe seam from + * proxy-egress-visibility.test.ts. + */ +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-egress-route-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_DISABLE_BACKGROUND_SERVICES = "true"; +process.env.API_KEY_SECRET = "test-api-key-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const proxyLogger = await import("../../src/lib/proxyLogger.ts"); +const localDb = await import("../../src/lib/localDb.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); +const route = await import("../../src/app/api/settings/proxies/egress/route.ts"); + +function resetStorage() { + core.resetDbInstance(); + proxyLogger.clearProxyLogs(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function setupAuth() { + process.env.INITIAL_PASSWORD = "bootstrap-password"; // pragma: allowlist secret (fixture du dépôt, même valeur que api-auth.test.ts) + await localDb.updateSettings({ requireLogin: true, password: "" }); + const key = await apiKeysDb.createApiKey("admin-key", "machine-test", ["manage"]); + return key.key; +} + +test.beforeEach(() => { + resetStorage(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("GET /api/settings/proxies/egress adds an anonymous summary to the existing payload", async () => { + const bearer = await setupAuth(); + + // Seed two codex accounts on one egress IP (persisted proxy_logs). + proxyLogger.logProxyEvent({ status: "success", provider: "codex", targetUrl: "codex/gpt-5.5", egressIp: "100.115.194.84", account: "acc-a", connectionId: "conn-a" }); + proxyLogger.logProxyEvent({ status: "success", provider: "codex", targetUrl: "codex/gpt-5.5", egressIp: "100.115.194.84", account: "acc-b", connectionId: "conn-b" }); + + const response = await route.GET(new Request("https://example.com/api/settings/proxies/egress", { + headers: { authorization: `Bearer ${bearer}` }, + })); + assert.equal(response.status, 200); + + const body = await response.json(); + + // Existing payload shape untouched. + assert.ok(Array.isArray(body.connections)); + assert.ok(body.byEgressIp && typeof body.byEgressIp === "object"); + assert.ok(Array.isArray(body.sharedWithinRotationGroup)); + + // Additive anonymous summary. + assert.equal(body.summary.distinctEgressIps, 1); + assert.equal(body.summary.maxAccountsSharingOneIp, 2); + assert.equal(body.summary.sharingByRotationGroup[0].rotationGroup, "openai-auth0"); + const json = JSON.stringify(body.summary); + assert.ok(!json.includes("100.115.194.84"), "no IP literal in the summary"); + assert.ok(!json.includes("acc-a"), "no account identity in the summary"); +}); + +test("GET returns 401 without a management token (auth untouched)", async () => { + // Self-contained auth setup (the reference api-auth.test.ts calls setupAuth + // inside each auth test) — no reliance on a previous test's env leakage. + process.env.INITIAL_PASSWORD = "bootstrap-password"; // pragma: allowlist secret (fixture du dépôt, même valeur que api-auth.test.ts) + await localDb.updateSettings({ requireLogin: true, password: "" }); + + const response = await route.GET(new Request("https://example.com/api/settings/proxies/egress")); + assert.equal(response.status, 401); +}); diff --git a/tests/unit/proxy-egress-summary.test.ts b/tests/unit/proxy-egress-summary.test.ts new file mode 100644 index 0000000000..6eddab01d1 --- /dev/null +++ b/tests/unit/proxy-egress-summary.test.ts @@ -0,0 +1,118 @@ +/** + * Anonymous egress-IP sharing summary (#10677). The pure aggregate + * must never leak IP literals or account identities — the redaction decision + * from #10348/#10539. Dedupes proxy_logs rows per account, reuses + * analyzeEgressSharing's rotation-group semantics. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { summarizeEgressSharing } = (await import("../../src/lib/proxyEgress.ts")) as unknown as { + summarizeEgressSharing: ( + rows: Array<{ + provider: string | null; + account: string | null; + connectionId: string | null; + egressIp: string | null; + }>, + window: { start: string; end: string } + ) => { + summary: { + windowStart: string; + windowEnd: string; + distinctEgressIps: number; + sharingByRotationGroup: Array<{ + rotationGroup: string; + sharedIps: number; + maxAccountsSharingOneIp: number; + }>; + maxAccountsSharingOneIp: number; + }; + warnings: Array<{ egressIp: string; rotationGroup: string; connections: string[] }>; + }; +}; + +const WINDOW = { start: "2026-08-20T00:00:00.000Z", end: "2026-08-21T00:00:00.000Z" }; +const row = ( + over: Partial<{ provider: string; account: string; connectionId: string; egressIp: string }> +) => ({ + provider: "codex", + account: "acc-a", + connectionId: "conn-a", + egressIp: "100.115.194.84", + ...over, +}); + +test("summarizeEgressSharing: two accounts of one rotation group on the same IP", () => { + const { summary: s } = summarizeEgressSharing( + [ + row({ account: "acc-a", connectionId: "conn-a" }), + row({ account: "acc-b", connectionId: "conn-b" }), + ], + WINDOW + ); + assert.equal(s.windowStart, WINDOW.start); + assert.equal(s.windowEnd, WINDOW.end); + assert.equal(s.distinctEgressIps, 1); + assert.equal(s.maxAccountsSharingOneIp, 2); + assert.equal(s.sharingByRotationGroup.length, 1); + assert.equal(s.sharingByRotationGroup[0].rotationGroup, "openai-auth0"); // codex+openai family + assert.equal(s.sharingByRotationGroup[0].sharedIps, 1); + assert.equal(s.sharingByRotationGroup[0].maxAccountsSharingOneIp, 2); +}); + +test("summarizeEgressSharing: repeated rows of the same account on one IP count once", () => { + const { summary: s } = summarizeEgressSharing( + [ + row({ account: "acc-a", connectionId: "conn-a" }), + row({ account: "acc-a", connectionId: "conn-a" }), + row({ account: "acc-b", connectionId: "conn-b" }), + row({ account: "acc-b", connectionId: "conn-b" }), + ], + WINDOW + ); + assert.equal(s.maxAccountsSharingOneIp, 2, "dedupe per account, not per request"); +}); + +test("summarizeEgressSharing: rows without egressIp are ignored", () => { + const { summary: s } = summarizeEgressSharing([row({ egressIp: null })], WINDOW); + assert.equal(s.distinctEgressIps, 0); + assert.equal(s.maxAccountsSharingOneIp, 0); + assert.deepEqual(s.sharingByRotationGroup, []); +}); + +test("summarizeEgressSharing: distinct IPs per account = no sharing", () => { + const { summary: s } = summarizeEgressSharing( + [ + row({ egressIp: "203.0.113.1" }), + row({ account: "acc-b", connectionId: "conn-b", egressIp: "203.0.113.2" }), + ], + WINDOW + ); + assert.equal(s.distinctEgressIps, 2); + assert.equal(s.sharingByRotationGroup.length, 0); +}); + +test("summarizeEgressSharing: summary carries counts only — no IP, no account", () => { + const { summary: s } = summarizeEgressSharing( + [row({}), row({ account: "acc-b", connectionId: "conn-b" })], + WINDOW + ); + const json = JSON.stringify(s); + assert.ok(!json.includes("100.115.194.84"), "no IP literal in the summary"); + assert.ok( + !json.includes("acc-a") && !json.includes("acc-b"), + "no account identity in the summary" + ); +}); + +test("summarizeEgressSharing: provider without rotation group falls back to provider:", () => { + const { summary: s } = summarizeEgressSharing( + [ + row({ provider: "weird-provider" }), + row({ provider: "weird-provider", account: "acc-b", connectionId: "conn-b" }), + ], + WINDOW + ); + assert.equal(s.sharingByRotationGroup[0].rotationGroup, "provider:weird-provider"); +}); diff --git a/tests/unit/proxy-health-egress-line.test.ts b/tests/unit/proxy-health-egress-line.test.ts new file mode 100644 index 0000000000..ce518ae665 --- /dev/null +++ b/tests/unit/proxy-health-egress-line.test.ts @@ -0,0 +1,119 @@ +/** + * The proxy-health sweep logs an anonymous egress-sharing + * summary line, computed from persisted proxy_logs (#10677) — no IP literals, no + * account identities (PROXY_LOG_INCLUDE_IPS is the only raw-detail opt-in, + * #10348). Console output is captured to prove the real sweep() emits it. + */ +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-egress-line-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.OMNIROUTE_DISABLE_BACKGROUND_SERVICES = "true"; +delete process.env.PROXY_LOG_INCLUDE_IPS; + +const core = await import("../../src/lib/db/core.ts"); +const proxyLogger = await import("../../src/lib/proxyLogger.ts"); +const proxiesDb = await import("../../src/lib/db/proxies.ts"); +const { forceProxyHealthSweep, formatEgressSharingSummaryLine } = await import( + "../../src/lib/proxyHealth/scheduler.ts" +) as unknown as { + forceProxyHealthSweep: () => Promise; + formatEgressSharingSummaryLine: ( + summary: EgressSharingSummary, + warnings: Array<{ egressIp: string; rotationGroup: string; connections: string[] }>, + includeDetails: boolean + ) => string; +}; +import type { EgressSharingSummary } from "../../src/lib/proxyEgress.ts"; + +function resetStorage() { + core.resetDbInstance(); + proxyLogger.clearProxyLogs(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + delete process.env.PROXY_LOG_INCLUDE_IPS; +}); + +test("formatEgressSharingSummaryLine is anonymous by default and raw when opted in", () => { + const summary: EgressSharingSummary = { + windowStart: "2026-08-20T00:00:00.000Z", + windowEnd: "2026-08-21T00:00:00.000Z", + distinctEgressIps: 1, + sharingByRotationGroup: [{ rotationGroup: "openai-auth0", sharedIps: 1, maxAccountsSharingOneIp: 2 }], + maxAccountsSharingOneIp: 2, + }; + const warnings = [{ egressIp: "100.115.194.84", rotationGroup: "openai-auth0", connections: ["a", "b"] }]; + + const anonymous = formatEgressSharingSummaryLine(summary, warnings, false); + assert.equal(anonymous, "[ProxyHealth] egress: 1 rotation group(s) share an egress IP (max 2 accounts)"); + assert.ok(!anonymous.includes("100.115.194.84"), "no IP without opt-in"); + + const raw = formatEgressSharingSummaryLine(summary, warnings, true); + assert.ok(raw.includes("100.115.194.84"), "raw IP only with PROXY_LOG_INCLUDE_IPS"); +}); + +test("forceProxyHealthSweep logs the anonymous egress line when accounts share an IP", async () => { + resetStorage(); + + await proxiesDb.createProxy({ + name: "Dead Local Proxy", + type: "http", + host: "127.0.0.1", + port: 1, // nothing listens — immediate ECONNREFUSED, no outbound traffic + }); + + // Two codex accounts on one egress IP, persisted (the sweep reads the DB). + proxyLogger.logProxyEvent({ status: "success", provider: "codex", targetUrl: "codex/gpt-5.5", egressIp: "100.115.194.84", account: "acc-a", connectionId: "conn-a" }); + proxyLogger.logProxyEvent({ status: "success", provider: "codex", targetUrl: "codex/gpt-5.5", egressIp: "100.115.194.84", account: "acc-b", connectionId: "conn-b" }); + + const logs: string[] = []; + const originalLog = console.log; + console.log = (...args: unknown[]) => { logs.push(args.join(" ")); }; + try { + await forceProxyHealthSweep(); + } finally { + console.log = originalLog; + } + + const line = logs.find((l) => l.includes("[ProxyHealth] egress:")); + assert.ok(line, "sweep must log the egress summary line"); + assert.ok(line!.includes("rotation group(s) share an egress IP (max 2 accounts)"), line!); + assert.ok(!line!.includes("100.115.194.84"), "no IP literal in the default sweep line"); + assert.ok(!line!.includes("acc-a"), "no account identity in the default sweep line"); +}); + +test("forceProxyHealthSweep logs raw details only with PROXY_LOG_INCLUDE_IPS=true", async () => { + resetStorage(); + process.env.PROXY_LOG_INCLUDE_IPS = "true"; + + await proxiesDb.createProxy({ + name: "Dead Local Proxy", + type: "http", + host: "127.0.0.1", + port: 1, + }); + proxyLogger.logProxyEvent({ status: "success", provider: "codex", targetUrl: "codex/gpt-5.5", egressIp: "100.115.194.84", account: "acc-a", connectionId: "conn-a" }); + proxyLogger.logProxyEvent({ status: "success", provider: "codex", targetUrl: "codex/gpt-5.5", egressIp: "100.115.194.84", account: "acc-b", connectionId: "conn-b" }); + + const logs: string[] = []; + const originalLog = console.log; + console.log = (...args: unknown[]) => { logs.push(args.join(" ")); }; + try { + await forceProxyHealthSweep(); + } finally { + console.log = originalLog; + } + + const line = logs.find((l) => l.includes("[ProxyHealth] egress:")); + assert.ok(line, "sweep must log the egress summary line"); + assert.ok(line!.includes("100.115.194.84"), "raw IP restored by the opt-in"); +}); From 7f90af645cc9e70286fbee09b4a15ecddc324e9c Mon Sep 17 00:00:00 2001 From: Aman <1402357+Zartharas@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:35:41 -0600 Subject: [PATCH 105/135] fix(db): remove stale MiMoCode state after provider sunset (#10873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — follow-up limpo do #10186: MiMoCode foi removido do OmniRoute, mas instalações que o configuraram antes da remoção retêm estado provider-scoped órfão (provider_connections, registered_keys, provider_key_limits, discovery_results, customModels). Migração de retirement segue o padrão explícito já usado para outros providers aposentados, preservando corretamente usage_history/call_logs históricos. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/migration-159-remove-mimocode-provider.test.ts — passando (mimocode + alias mcode removidos, estado de outros providers preservado, idempotência, histórico preservado) --- ...10873-mimocode-retirement-state-cleanup.md | 1 + .../159_remove_mimocode_provider.sql | 22 +++ ...ation-159-remove-mimocode-provider.test.ts | 158 ++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md create mode 100644 src/lib/db/migrations/159_remove_mimocode_provider.sql create mode 100644 tests/unit/migration-159-remove-mimocode-provider.test.ts diff --git a/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md b/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md new file mode 100644 index 0000000000..44443eac6c --- /dev/null +++ b/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md @@ -0,0 +1 @@ +- **fix(db):** Remove stale MiMoCode provider configuration, including the legacy `mcode` alias, left after provider retirement while preserving historical usage and call logs ([#10873](https://github.com/diegosouzapw/OmniRoute/pull/10873)) — thanks @Zartharas diff --git a/src/lib/db/migrations/159_remove_mimocode_provider.sql b/src/lib/db/migrations/159_remove_mimocode_provider.sql new file mode 100644 index 0000000000..1775d31333 --- /dev/null +++ b/src/lib/db/migrations/159_remove_mimocode_provider.sql @@ -0,0 +1,22 @@ +-- 159_remove_mimocode_provider.sql +-- MiMoCode was removed from OmniRoute, but installations that configured it +-- before removal can retain provider-scoped state. Remove that stale +-- configuration for both the canonical provider id and its historical alias. +-- +-- Historical request, usage, and call-log records are intentionally preserved. + +DELETE FROM provider_connections +WHERE provider IN ('mimocode', 'mcode'); + +DELETE FROM registered_keys +WHERE provider IN ('mimocode', 'mcode'); + +DELETE FROM provider_key_limits +WHERE provider IN ('mimocode', 'mcode'); + +DELETE FROM discovery_results +WHERE provider_id IN ('mimocode', 'mcode'); + +DELETE FROM key_value +WHERE namespace = 'customModels' + AND key IN ('mimocode', 'mcode'); diff --git a/tests/unit/migration-159-remove-mimocode-provider.test.ts b/tests/unit/migration-159-remove-mimocode-provider.test.ts new file mode 100644 index 0000000000..ce62675fde --- /dev/null +++ b/tests/unit/migration-159-remove-mimocode-provider.test.ts @@ -0,0 +1,158 @@ +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-mimocode-retirement-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("migration 159 removes stale MiMoCode provider state and is idempotent", () => { + const db = core.getDbInstance(); + + const applied = db + .prepare("SELECT version FROM _omniroute_migrations WHERE version = 159") + .get() as { version: number } | undefined; + + assert.ok(applied, "migration 159 must be recorded as applied"); + + for (const provider of ["mimocode", "mcode"]) { + db.prepare( + "INSERT INTO provider_connections " + + "(id, provider, auth_type, name, is_active, created_at, updated_at) " + + "VALUES (?, ?, ?, ?, 1, datetime('now'), datetime('now'))" + ).run(`${provider}-connection`, provider, "apikey", `${provider}-legacy`); + + db.prepare( + "INSERT INTO registered_keys " + + "(id, key, key_prefix, name, provider, account_id) " + + "VALUES (?, ?, ?, ?, ?, ?)" + ).run( + `${provider}-key-id`, + `${provider}-key-hash`, + `${provider.slice(0, 8)}`, + `${provider}-key`, + provider, + `${provider}-account` + ); + + db.prepare("INSERT INTO provider_key_limits (provider) VALUES (?)").run(provider); + + db.prepare( + "INSERT INTO discovery_results " + + "(provider_id, method, endpoint, auth_type) " + + "VALUES (?, 'public_api', ?, 'api_key')" + ).run(provider, `https://${provider}.example.invalid/v1`); + + db.prepare( + "INSERT INTO key_value (namespace, key, value) " + "VALUES ('customModels', ?, '[]')" + ).run(provider); + + db.prepare( + "INSERT INTO usage_history (provider, model, timestamp) " + + "VALUES (?, 'legacy-model', datetime('now'))" + ).run(provider); + + db.prepare( + "INSERT INTO call_logs (id, timestamp, provider, model, status) " + + "VALUES (?, datetime('now'), ?, 'legacy-model', 200)" + ).run(`${provider}-historical-call`, provider); + } + + db.prepare( + "INSERT INTO provider_connections " + + "(id, provider, auth_type, name, is_active, created_at, updated_at) " + + "VALUES ('openai-control', 'openai', 'apikey', 'control', 1, datetime('now'), datetime('now'))" + ).run(); + + db.prepare( + "INSERT INTO registered_keys " + + "(id, key, key_prefix, name, provider, account_id) " + + "VALUES ('openai-key-id', 'openai-key-hash', 'openai', 'control', 'openai', 'control-account')" + ).run(); + + db.prepare("INSERT INTO provider_key_limits (provider) VALUES ('openai')").run(); + + db.prepare( + "INSERT INTO discovery_results " + + "(provider_id, method, endpoint, auth_type) " + + "VALUES ('openai', 'public_api', 'https://openai.example.invalid/v1', 'api_key')" + ).run(); + + db.prepare( + "INSERT INTO key_value (namespace, key, value) " + "VALUES ('customModels', 'openai', '[]')" + ).run(); + + const sql = fs.readFileSync( + path.join(process.cwd(), "src/lib/db/migrations/159_remove_mimocode_provider.sql"), + "utf8" + ); + + db.exec(sql); + db.exec(sql); + + for (const provider of ["mimocode", "mcode"]) { + assert.equal( + db.prepare("SELECT id FROM provider_connections WHERE provider = ?").get(provider), + undefined, + `${provider} provider_connections rows must be deleted` + ); + + assert.equal( + db.prepare("SELECT id FROM registered_keys WHERE provider = ?").get(provider), + undefined, + `${provider} registered_keys rows must be deleted` + ); + + assert.equal( + db.prepare("SELECT provider FROM provider_key_limits WHERE provider = ?").get(provider), + undefined, + `${provider} provider_key_limits rows must be deleted` + ); + + assert.equal( + db.prepare("SELECT id FROM discovery_results WHERE provider_id = ?").get(provider), + undefined, + `${provider} discovery_results rows must be deleted` + ); + + assert.equal( + db + .prepare("SELECT key FROM key_value WHERE namespace = 'customModels' AND key = ?") + .get(provider), + undefined, + `${provider} custom models must be deleted` + ); + + assert.ok( + db.prepare("SELECT id FROM usage_history WHERE provider = ?").get(provider), + `${provider} historical usage must be preserved` + ); + + assert.ok( + db.prepare("SELECT id FROM call_logs WHERE provider = ?").get(provider), + `${provider} historical call logs must be preserved` + ); + } + + assert.ok(db.prepare("SELECT id FROM provider_connections WHERE provider = 'openai'").get()); + + assert.ok(db.prepare("SELECT id FROM registered_keys WHERE provider = 'openai'").get()); + + assert.ok(db.prepare("SELECT provider FROM provider_key_limits WHERE provider = 'openai'").get()); + + assert.ok(db.prepare("SELECT id FROM discovery_results WHERE provider_id = 'openai'").get()); + + assert.ok( + db + .prepare("SELECT key FROM key_value WHERE namespace = 'customModels' AND key = 'openai'") + .get() + ); +}); From 0b51a242ce51d17b60b9a465b915a78b55433c24 Mon Sep 17 00:00:00 2001 From: Aman <1402357+Zartharas@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:35:51 -0600 Subject: [PATCH 106/135] fix(provider-health): keep unsupported validation probes neutral (#10878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — corrige um caso real onde um 404/405 no chat-probe de validação genérica OpenAI-like era tratado como credencial inválida, quando na verdade significa apenas que o provider não expõe essa superfície de validação. Agora o validador retorna `unsupported: true`, a rota de teste responde `skipped: true`, a saúde persistida não é reescrita, o cache de CredentialHealth não é poluído, e o scheduler ainda respeita o healthCheckInterval configurado — sem introduzir exceção específica de provider nem tocar comportamento do MiMoCode. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/provider-validation-unsupported-neutral.test.ts — passando (TDD RED→GREEN completo: classificação do validador, não-mutação de saúde persistida, pacing do scheduler, regressão de lease-skip, comportamento existente de 403/429 preservado) --- ...8-unsupported-validation-probes-neutral.md | 1 + src/app/api/providers/[id]/test/route.ts | 13 ++ src/lib/credentialHealth/scheduler.ts | 16 +- src/lib/providers/validation/openaiFormat.ts | 6 +- ...der-validation-unsupported-neutral.test.ts | 209 ++++++++++++++++++ 5 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/10878-unsupported-validation-probes-neutral.md create mode 100644 tests/unit/provider-validation-unsupported-neutral.test.ts diff --git a/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md b/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md new file mode 100644 index 0000000000..1fc7c01933 --- /dev/null +++ b/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md @@ -0,0 +1 @@ +- **fix(provider-health):** Keep unsupported 404/405 validation probes neutral so they do not poison stored credential health or scheduler failure state, while still honoring per-connection health-check pacing ([#10878](https://github.com/diegosouzapw/OmniRoute/pull/10878)) — thanks @Zartharas diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index 36effb38a6..ec1b235d5e 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -704,6 +704,7 @@ async function testApiKeyConnection(connection: any) { const error = "Provider test not supported"; return { valid: false, + skipped: true, error, diagnosis: classifyFailure({ error, unsupported: true, provider: connection.provider }), }; @@ -796,6 +797,18 @@ export async function testSingleConnection(connectionId: string, validationModel const latencyMs = Date.now() - startTime; + // Unsupported validation capability is neutral: the probe established that + // this provider cannot be verified through the generic test surface, not + // that its credential is invalid. Do not mutate persisted credential health. + if (result.skipped === true) { + return { + ...result, + latencyMs, + runtime: runtime || null, + testedAt: null, + }; + } + // Build update data const now = new Date().toISOString(); const diagnosis = diff --git a/src/lib/credentialHealth/scheduler.ts b/src/lib/credentialHealth/scheduler.ts index 7eff86c4df..8dfca2f595 100644 --- a/src/lib/credentialHealth/scheduler.ts +++ b/src/lib/credentialHealth/scheduler.ts @@ -130,8 +130,20 @@ async function testConnection( try { const result = await testSingleConnection(connectionId); - // A deliberate lease skip must not rewrite the health cache. - if (result.skipped === true) return; + // Deliberate skips never rewrite credential health or failure state. + // Unsupported validation capability is stable enough to honor the + // connection's configured interval; an exclusive-lease skip intentionally + // remains due on the next global sweep so recovery is not delayed. + if (result.skipped === true) { + const diagnosis = result.diagnosis as { code?: string } | undefined; + if (diagnosis?.code === "unsupported") { + getSchedulerState().perConnTiming.set(connectionId, { + lastAttemptAt: startTime, + nextAttemptAt: startTime + intervalMs, + }); + } + return; + } const latencyMs = Date.now() - startTime; const state = getSchedulerState(); diff --git a/src/lib/providers/validation/openaiFormat.ts b/src/lib/providers/validation/openaiFormat.ts index 9f6efa9203..3d60113134 100644 --- a/src/lib/providers/validation/openaiFormat.ts +++ b/src/lib/providers/validation/openaiFormat.ts @@ -163,7 +163,11 @@ export async function validateOpenAILikeProvider({ } if (chatRes.status === 404 || chatRes.status === 405) { - return { valid: false, error: "Provider validation endpoint not supported" }; + return { + valid: false, + error: "Provider validation endpoint not supported", + unsupported: true, + }; } if (chatRes.status >= 500) { diff --git a/tests/unit/provider-validation-unsupported-neutral.test.ts b/tests/unit/provider-validation-unsupported-neutral.test.ts new file mode 100644 index 0000000000..d26433dfb0 --- /dev/null +++ b/tests/unit/provider-validation-unsupported-neutral.test.ts @@ -0,0 +1,209 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const TEST_DATA_DIR = fs.mkdtempSync( + path.join(os.tmpdir(), "omniroute-unsupported-probe-neutral-") +); + +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK = "true"; + +const originalFetch = globalThis.fetch; + +const core = await import("../../src/lib/db/core.ts"); + +const { validateOpenAILikeProvider } = + await import("../../src/lib/providers/validation/openaiFormat.ts"); + +const { testSingleConnection } = await import("../../src/app/api/providers/[id]/test/route.ts"); + +const credentialHealthScheduler = await import("../../src/lib/credentialHealth/scheduler.ts"); + +function mockUnsupportedValidationSurface() { + let calls = 0; + + globalThis.fetch = (async (url: string | URL | Request) => { + calls += 1; + + const href = typeof url === "string" ? url : url instanceof URL ? url.href : url.url; + + if (href.includes("/models")) { + return new Response("not found", { + status: 404, + }); + } + + return new Response("method not allowed", { + status: 405, + }); + }) as typeof fetch; + + return () => calls; +} + +test.after(() => { + credentialHealthScheduler.stopCredentialHealthCheck(); + globalThis.fetch = originalFetch; + core.resetDbInstance(); + + fs.rmSync(TEST_DATA_DIR, { + recursive: true, + force: true, + }); +}); + +test("404/405 OpenAI-like probe surface is explicitly unsupported, not an auth failure", async () => { + const getCalls = mockUnsupportedValidationSurface(); + + const result = (await validateOpenAILikeProvider({ + provider: "openai", + apiKey: "synthetic-test-key", + baseUrl: "https://api.openai.com/v1", + modelId: "gpt-4.1-mini", + providerSpecificData: {}, + })) as { + valid: boolean; + error: string | null; + unsupported?: boolean; + }; + + assert.equal(getCalls(), 2, "expected /models then chat/completions validation probes"); + + assert.equal(result.valid, false); + + assert.equal(result.unsupported, true, "404/405 validation surface must carry unsupported:true"); + + assert.match(String(result.error), /validation endpoint not supported/i); +}); + +test("unsupported provider verification is neutral and does not poison stored connection health", async () => { + const getCalls = mockUnsupportedValidationSurface(); + + const db = core.getDbInstance(); + + db.prepare( + `INSERT INTO provider_connections + ( + id, + provider, + auth_type, + name, + api_key, + is_active, + test_status, + health_check_interval, + created_at, + updated_at + ) + VALUES + (?, ?, 'apikey', ?, ?, 1, 'active', 60, ?, ?)` + ).run( + "unsupported-probe-test", + "openai", + "unsupported probe test", + "synthetic-test-key", + new Date().toISOString(), + new Date().toISOString() + ); + + const before = db + .prepare( + `SELECT + test_status, + last_tested, + last_error, + last_error_at, + error_code + FROM provider_connections + WHERE id = ?` + ) + .get("unsupported-probe-test") as { + test_status: string; + last_tested: string | null; + last_error: string | null; + last_error_at: string | null; + error_code: string | null; + }; + + assert.equal(before.test_status, "active"); + assert.equal(before.last_tested, null); + assert.equal(before.last_error, null); + assert.equal(before.last_error_at, null); + assert.equal(before.error_code, null); + + const result = await testSingleConnection("unsupported-probe-test"); + + assert.equal( + getCalls(), + 2, + "expected actual validator execution before capability was classified unsupported" + ); + + assert.equal(result.valid, false); + + assert.equal(result.skipped, true, "unsupported verification must be returned as a neutral skip"); + + assert.equal(result.diagnosis?.code, "unsupported"); + + const after = db + .prepare( + `SELECT + test_status, + last_tested, + last_error, + last_error_at, + error_code + FROM provider_connections + WHERE id = ?` + ) + .get("unsupported-probe-test") as { + test_status: string; + last_tested: string | null; + last_error: string | null; + last_error_at: string | null; + error_code: string | null; + }; + + assert.deepEqual( + after, + before, + "unsupported capability detection must not mutate persisted credential health" + ); + + await credentialHealthScheduler.forceSweep(); + + const timing = globalThis.__omnirouteCredentialHC?.perConnTiming.get("unsupported-probe-test"); + + assert.ok(timing, "unsupported scheduler skip must still establish per-connection pacing"); + + assert.equal( + timing.nextAttemptAt - timing.lastAttemptAt, + 60 * 60_000, + "unsupported validation must honor the connection's 60-minute health-check interval" + ); + + const afterSweep = db + .prepare( + `SELECT + test_status, + last_tested, + last_error, + last_error_at, + error_code + FROM provider_connections + WHERE id = ?` + ) + .get("unsupported-probe-test"); + + assert.deepEqual( + afterSweep, + before, + "scheduler handling of unsupported validation must remain health-state neutral" + ); + + credentialHealthScheduler.stopCredentialHealthCheck(); +}); From c40ff16a1d2f1199fe913b46a0a110650cd8e990 Mon Sep 17 00:00:00 2001 From: Aman <1402357+Zartharas@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:42:02 -0600 Subject: [PATCH 107/135] fix(providers): preserve health on inconclusive probes (#10799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — distinção precisa entre saúde de credencial e disponibilidade upstream: timeout do NVIDIA e HTTP 400 genérico do Antigravity/AGY passavam a poluir a saúde da credencial como se fossem falha de autenticação, quando na verdade são resultados inconclusivos. Preserva o path explícito de geo-block do Google (esse continua indo pelo tratamento de geo/egress existente). Reconciliado nesta sessão contra o release tip atualizado (pós #10878/#10873) — conflito real em scheduler.ts resolvido de forma additiva (pacing por intervalo do release + recheck mais lento para probes inconclusivos empilhados). Validação (reconciliação a partir de origin/release/v3.8.50, gates estáticos rebaselineados para a soma legítima de #10878+#10799 no test/route.ts): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/provider-health-inconclusive-probes.test.ts + nvidia-nim-validator.test.ts + antigravity-geoblock-resilience.test.ts — 22/22 passando --- ...799-provider-health-inconclusive-probes.md | 1 + config/quality/file-size-baseline.json | 2 + .../providers/[id]/test/oauthTestConfig.ts | 37 ++++++ src/app/api/providers/[id]/test/route.ts | 67 ++++++++++- src/lib/credentialHealth/probePolicy.ts | 23 ++++ src/lib/credentialHealth/scheduler.ts | 28 ++++- .../providers/validation/specialtyInline.ts | 15 ++- ...rovider-health-inconclusive-probes.test.ts | 107 ++++++++++++++++++ 8 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 changelog.d/fixes/10799-provider-health-inconclusive-probes.md create mode 100644 src/lib/credentialHealth/probePolicy.ts create mode 100644 tests/unit/provider-health-inconclusive-probes.test.ts diff --git a/changelog.d/fixes/10799-provider-health-inconclusive-probes.md b/changelog.d/fixes/10799-provider-health-inconclusive-probes.md new file mode 100644 index 0000000000..72aacacee0 --- /dev/null +++ b/changelog.d/fixes/10799-provider-health-inconclusive-probes.md @@ -0,0 +1 @@ +- **fix(providers):** Keep NVIDIA timeout probes and generic Antigravity/AGY HTTP 400 probes from poisoning credential health while preserving explicit Google geo-block handling ([#10799](https://github.com/diegosouzapw/OmniRoute/pull/10799)) — thanks @Zartharas diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 77d54f7845..900f6220f8 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -304,6 +304,8 @@ "_rebaseline_2026_07_27_3850_relax_filesize_cap": "OWNER-APPROVED TEMPORARY relax for v3.8.50-3.8.54 PREPARE phase (docs/ROADMAP.md). cap 800->900 (+100), testCap 800->900 (+100). Targets: decompose-existing-frozen unchanged (frozen still only-shrink); this only relaxes the cap for NEW files in the decompose/extract-while-PREPARE phase (.51='executor registry in-place' and .52='combo.ts decomposition' create new leaf modules above 800). RE-TIGHTENING MANDATORY in v3.8.51: cap target 850 = 850 once decomposition wave stabilizes. SUPERSEDED by _rebaseline_2026_07_27_3850_relax_filesize_cap_v2_20pct (v1 +20% buffer) — retained for audit. Tracked via same roadmap issue.", "_rebaseline_2026_07_27_v3849_train1h": "Merge-train 1H (31 PRs) — owner-approved 2026-07-27. Two distinct causes, kept separate on purpose: (1) GENUINE irreducible growth at existing chokepoints — providerLimits/auth (#8632 Kimi quota-reset recovery), rateLimitManager (#8616 idle wedged limiters), models-catalog-route.test (#8610 OpenCode Go effort aliases); (2) COLLISION with #8585, which banked shrinks measured on the pre-train release tip while 30 sibling PRs in the SAME train grew those files again — chat/accountFallback (#8628), chatCore (#8613), videoGeneration (#8581), imageGeneration. The zero-headroom frozen entries cannot absorb either. Ceilings re-pinned to the post-merge tip; #8612 (also in this train) automates shrink-banking so this self-inflicted drift stops recurring. Detail: src/lib/usage/providerLimits.ts 1006->1013 (#8632); src/sse/services/auth.ts 2492->2508 (#8632); open-sse/services/rateLimitManager.ts 1014->1060 (#8616); src/sse/handlers/chat.ts 1842->1845 (#8628); open-sse/handlers/chatCore.ts 4939->4955 (#8613); open-sse/handlers/imageGeneration.ts 3100->3101 ((sem PR — teto do #8585)); open-sse/handlers/videoGeneration.ts 1038->1063 (#8581); open-sse/services/accountFallback.ts 1965->1966 (#8628); tests/unit/models-catalog-route.test.ts 1608->1636 (#8610)", "frozen": { + "_rebaseline_2026_08_20_10878_10799_provider_health_probes": "PRs #10878 (unsupported OpenAI-like validation probes stay neutral) + #10799 (preserve credential health on inconclusive NVIDIA-timeout/Antigravity-400 probes) own growth: src/app/api/providers/[id]/test/route.ts 946->1025 (+79, sum of both boarded together). Both add narrowly-scoped classification branches at the existing test-route dispatch chokepoint (unsupported-capability skip, credential-inconclusive detection) rather than new files, mirroring the prior 2026_06_27_5193 rebaseline of the same file. Covered by tests/unit/provider-validation-unsupported-neutral.test.ts + tests/unit/provider-health-inconclusive-probes.test.ts.", + "src/app/api/providers/[id]/test/route.ts": 1025, "_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.", "_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.", "_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, ; body?: string; acceptStatuses?: number[]; + inconclusiveStatuses?: number[]; checkExpiry?: boolean; refreshable?: boolean; getUrl?: (connection: any) => string; @@ -88,6 +90,39 @@ export interface OAuthTestConfigEntry { ) => OAuthTestProbeRequest | Promise; } +export interface OAuthProbeInconclusiveClassification { + warning: string; + diagnosisType: "ok"; + diagnosisCode: "probe_inconclusive"; +} + +export function classifyOAuthProbeInconclusive( + config: OAuthTestConfigEntry, + provider: string, + status: number, + bodyText: string +): OAuthProbeInconclusiveClassification | null { + if ( + !Array.isArray(config.inconclusiveStatuses) || + !config.inconclusiveStatuses.includes(status) + ) { + return null; + } + + // Preserve the current upstream geo-block contract. Google's explicit + // location refusal is an egress/upstream availability failure, not a + // successful connection-test result. + if ((provider === "antigravity" || provider === "agy") && isGeoBlockedError(bodyText)) { + return null; + } + + return { + warning: `${provider} probe returned HTTP ${status}; credential validity is inconclusive`, + diagnosisType: "ok", + diagnosisCode: "probe_inconclusive", + }; +} + export const OAUTH_TEST_CONFIG: Record = { claude: { // Claude doesn't have userinfo, we verify token exists and not expired @@ -126,6 +161,7 @@ export const OAUTH_TEST_CONFIG: Record = { // Real model-surface probe (see buildAntigravityProbe above): userinfo-only // probing stayed green while the model API was geo-blocked. buildProbe: buildAntigravityProbe, + inconclusiveStatuses: [400], refreshable: true, }, // `agy` is a separate connection id that shares the Antigravity backend and the same @@ -135,6 +171,7 @@ export const OAUTH_TEST_CONFIG: Record = { // perfectly good account. Probe the same model surface as antigravity. agy: { buildProbe: buildAntigravityProbe, + inconclusiveStatuses: [400], refreshable: true, }, xai: XAI_CHAT_OAUTH_TEST_CONFIG, diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index ec1b235d5e..7ac785c7f5 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -32,7 +32,7 @@ import { removeConnectionHealth } from "@omniroute/open-sse/services/apiKeyRotat import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation"; import { classifyAmbiguousOrAuthError, type ClassifyFailureArgs } from "./mistralAmbiguousAuth"; import { buildApiKeyConnectionTestResult } from "./apiKeyTestResult"; -import { OAUTH_TEST_CONFIG } from "./oauthTestConfig"; +import { classifyOAuthProbeInconclusive, OAUTH_TEST_CONFIG } from "./oauthTestConfig"; import { isGeoBlockedError } from "@omniroute/open-sse/services/errorClassifier.ts"; // Bound the OAuth probe so a hung upstream can't block the connection-test queue @@ -511,6 +511,38 @@ export async function testOAuthConnection( if (builtProbe?.body) fetchInit.body = builtProbe.body; const res = await fetch(url, fetchInit); + const inconclusiveBody = + Array.isArray(config.inconclusiveStatuses) && config.inconclusiveStatuses.includes(res.status) + ? await res + .clone() + .text() + .catch(() => "") + : ""; + + const inconclusive = classifyOAuthProbeInconclusive( + config, + connection.provider, + res.status, + inconclusiveBody + ); + + if (inconclusive) { + return { + valid: true, + error: null, + warning: inconclusive.warning, + refreshed, + newTokens, + statusCode: res.status, + diagnosis: makeDiagnosis( + inconclusive.diagnosisType, + "upstream", + inconclusive.warning, + inconclusive.diagnosisCode + ), + }; + } + // Port of decolua/9router#347: some providers (Codex) intentionally trigger a // 400 because the probe body is invalid. A 400 from such a provider means auth // succeeded; only 401/403 means the token is bad. @@ -573,6 +605,39 @@ export async function testOAuthConnection( else if (config.body) retryInit.body = config.body; const retryRes = await fetch(url, retryInit); + const retryInconclusiveBody = + Array.isArray(config.inconclusiveStatuses) && + config.inconclusiveStatuses.includes(retryRes.status) + ? await retryRes + .clone() + .text() + .catch(() => "") + : ""; + + const retryInconclusive = classifyOAuthProbeInconclusive( + config, + connection.provider, + retryRes.status, + retryInconclusiveBody + ); + + if (retryInconclusive) { + return { + valid: true, + error: null, + warning: retryInconclusive.warning, + refreshed: true, + newTokens: tokens, + statusCode: retryRes.status, + diagnosis: makeDiagnosis( + retryInconclusive.diagnosisType, + "upstream", + retryInconclusive.warning, + retryInconclusive.diagnosisCode + ), + }; + } + const retryAccepted = retryRes.ok || (Array.isArray(config.acceptStatuses) && config.acceptStatuses.includes(retryRes.status)); diff --git a/src/lib/credentialHealth/probePolicy.ts b/src/lib/credentialHealth/probePolicy.ts new file mode 100644 index 0000000000..6b4d387e36 --- /dev/null +++ b/src/lib/credentialHealth/probePolicy.ts @@ -0,0 +1,23 @@ +const DEFAULT_SWEEP_INTERVAL_MS = 300_000; +const INCONCLUSIVE_RECHECK_MIN_MS = 30 * 60_000; +const INCONCLUSIVE_RECHECK_MULTIPLIER = 6; +const INCONCLUSIVE_WARNING_MARKER = "credential validity is inconclusive"; + +export function isCredentialProbeInconclusive(result: { + valid?: boolean; + warning?: unknown; +}): boolean { + return ( + result.valid === true && + typeof result.warning === "string" && + result.warning.toLowerCase().includes(INCONCLUSIVE_WARNING_MARKER) + ); +} + +export function resolveInconclusiveProbeRecheckDelayMs(sweepIntervalMs: number): number { + const interval = + Number.isFinite(sweepIntervalMs) && sweepIntervalMs > 0 + ? sweepIntervalMs + : DEFAULT_SWEEP_INTERVAL_MS; + return Math.max(INCONCLUSIVE_RECHECK_MIN_MS, interval * INCONCLUSIVE_RECHECK_MULTIPLIER); +} diff --git a/src/lib/credentialHealth/scheduler.ts b/src/lib/credentialHealth/scheduler.ts index 8dfca2f595..a407f6a326 100644 --- a/src/lib/credentialHealth/scheduler.ts +++ b/src/lib/credentialHealth/scheduler.ts @@ -24,6 +24,10 @@ import { removeCredentialHealth, initCredentialCache, } from "@/lib/credentialHealth/cache"; +import { + isCredentialProbeInconclusive, + resolveInconclusiveProbeRecheckDelayMs, +} from "@/lib/credentialHealth/probePolicy"; import { emit } from "@/lib/events/eventBus"; import { isAutomatedTestProcess } from "@/shared/utils/testProcess"; import { SEARCH_VALIDATOR_CONFIGS } from "@/lib/providers/validation/searchProviders"; @@ -149,13 +153,25 @@ async function testConnection( const state = getSchedulerState(); if (result.valid) { - // Success — reset failure count, space the next test by the - // per-connection interval (absent → global sweep interval), update cache + // Success resets failure state. Credential-inconclusive probes remain + // active but are checked less often because repeating an expensive probe + // does not add authentication evidence; an ordinary success is paced by + // the per-connection interval (absent → global sweep interval). state.failureCounts.delete(connectionId); - state.perConnTiming.set(connectionId, { - lastAttemptAt: startTime, - nextAttemptAt: startTime + intervalMs, - }); + + if (isCredentialProbeInconclusive(result)) { + const recheckDelayMs = resolveInconclusiveProbeRecheckDelayMs(getSweepInterval()); + state.perConnTiming.set(connectionId, { + lastAttemptAt: startTime, + nextAttemptAt: Date.now() + recheckDelayMs, + }); + } else { + state.perConnTiming.set(connectionId, { + lastAttemptAt: startTime, + nextAttemptAt: startTime + intervalMs, + }); + } + setCredentialHealth( connectionId, provider, diff --git a/src/lib/providers/validation/specialtyInline.ts b/src/lib/providers/validation/specialtyInline.ts index 7729fa4554..fc9102f5cd 100644 --- a/src/lib/providers/validation/specialtyInline.ts +++ b/src/lib/providers/validation/specialtyInline.ts @@ -223,6 +223,19 @@ export async function validateLongcatProvider({ apiKey, providerSpecificData, is } } +export function normalizeNvidiaValidationFailure(error: unknown) { + const failure = toValidationErrorResult(error); + if (failure.timeout) { + return { + valid: true, + error: null, + warning: "NVIDIA auth probe timed out; credential validity is inconclusive", + method: "chat_probe_inconclusive", + }; + } + return failure; +} + // NVIDIA NIM (#2463) — bypass the /models probe in favor of a direct // chat/completions probe. NVIDIA NIM's /models endpoint returns model // catalogs that vary by region and key-tier, and some keys 404 on it, @@ -262,7 +275,7 @@ export async function validateNvidiaProvider({ apiKey, providerSpecificData }: a // Any non-auth response (200, 400, 422, 429) means auth passed return { valid: true, error: null }; } catch (error: any) { - return toValidationErrorResult(error); + return normalizeNvidiaValidationFailure(error); } } diff --git a/tests/unit/provider-health-inconclusive-probes.test.ts b/tests/unit/provider-health-inconclusive-probes.test.ts new file mode 100644 index 0000000000..f6d693a847 --- /dev/null +++ b/tests/unit/provider-health-inconclusive-probes.test.ts @@ -0,0 +1,107 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { SafeOutboundFetchError } from "../../src/shared/network/safeOutboundFetch.ts"; +import { normalizeNvidiaValidationFailure } from "../../src/lib/providers/validation/specialtyInline.ts"; +import { + classifyOAuthProbeInconclusive, + OAUTH_TEST_CONFIG, +} from "../../src/app/api/providers/[id]/test/oauthTestConfig.ts"; +import { + isCredentialProbeInconclusive, + resolveInconclusiveProbeRecheckDelayMs, +} from "../../src/lib/credentialHealth/probePolicy.ts"; + +test("NVIDIA timeout probe is credential-inconclusive instead of an auth failure", () => { + const error = new SafeOutboundFetchError( + "Request to https://integrate.api.nvidia.com/v1/chat/completions timed out after 20000ms", + { + code: "TIMEOUT", + url: "https://integrate.api.nvidia.com/v1/chat/completions", + method: "POST", + attempts: 1, + isRetryable: true, + timeoutMs: 20_000, + } + ); + + const result = normalizeNvidiaValidationFailure(error) as { + valid: boolean; + error: string | null; + method?: string; + warning?: string; + }; + + assert.equal(result.valid, true); + assert.equal(result.error, null); + assert.equal(result.method, "chat_probe_inconclusive"); + assert.match(String(result.warning), /credential validity is inconclusive/i); + assert.equal(isCredentialProbeInconclusive(result), true); +}); + +test("NVIDIA non-timeout network failures remain failures", () => { + const error = new SafeOutboundFetchError("fetch failed", { + code: "NETWORK_ERROR", + url: "https://integrate.api.nvidia.com/v1/chat/completions", + method: "POST", + attempts: 1, + isRetryable: true, + }); + + const result = normalizeNvidiaValidationFailure(error); + + assert.equal(result.valid, false); + assert.equal(result.error, "fetch failed"); + assert.equal(isCredentialProbeInconclusive(result), false); +}); + +test("inconclusive probes back off without changing ordinary successful probes", () => { + assert.equal( + isCredentialProbeInconclusive({ valid: true, warning: "ordinary provider warning" }), + false + ); + assert.equal( + isCredentialProbeInconclusive({ + valid: false, + warning: "credential validity is inconclusive", + }), + false + ); + assert.equal(resolveInconclusiveProbeRecheckDelayMs(300_000), 1_800_000); + assert.equal(resolveInconclusiveProbeRecheckDelayMs(600_000), 3_600_000); +}); + +test("Antigravity and AGY keep generic HTTP 400 inconclusive while preserving geo-block failures", () => { + for (const provider of ["antigravity", "agy"] as const) { + const config = OAUTH_TEST_CONFIG[provider]; + + assert.deepEqual(config?.inconclusiveStatuses, [400]); + assert.ok(!config?.acceptStatuses?.includes(400)); + + const generic = classifyOAuthProbeInconclusive(config, provider, 400, ""); + + assert.ok(generic); + assert.equal(generic.diagnosisCode, "probe_inconclusive"); + assert.match(generic.warning, /credential validity is inconclusive/i); + assert.equal( + isCredentialProbeInconclusive({ + valid: true, + warning: generic.warning, + }), + true + ); + + assert.equal( + classifyOAuthProbeInconclusive( + config, + provider, + 400, + '{"error":"User location is not supported for the API use."}' + ), + null, + "explicit Google geo-blocks must fall through to upstream availability handling" + ); + + assert.equal(classifyOAuthProbeInconclusive(config, provider, 401, ""), null); + assert.equal(classifyOAuthProbeInconclusive(config, provider, 403, ""), null); + } +}); From 25ba4f2a3425daf57cdeef672a80a815e2c10d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:30:43 +0330 Subject: [PATCH 108/135] feat(antigravity): auto-rotate BYOP accounts to siblings on GCP_PROJECT_REQUIRED (#10470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — follow-up bem feito do #10424: um 422 GCP_PROJECT_REQUIRED (BYOP — a conta Google precisa trazer seu próprio GCP Project) é específico da CONTA, não do provider inteiro, então o fast-fail anterior falhava mesmo quando uma conta antigravity irmã saudável poderia atender o request. Agora rotaciona automaticamente para a conta irmã (excluindo a conta BYOP por 24h) e só surfaça o erro acionável quando não há irmã disponível. Estado de rotação rastreado separado de maxAttempts, então falhas normais do antigravity nunca ganham uma segunda chance (sem dispatch duplo). Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo — auto-merge em chatCore.ts, 0 conflitos reais): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/antigravity-byop-account-rotation.test.ts + error-classifier.test.ts — 36/36 passando --- ...10470-antigravity-byop-account-rotation.md | 1 + open-sse/config/errorConfig.ts | 4 + open-sse/handlers/chatCore.ts | 83 +++++- open-sse/services/errorClassifier.ts | 13 + .../antigravity-byop-account-rotation.test.ts | 253 ++++++++++++++++++ tests/unit/error-classifier.test.ts | 33 +++ 6 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10470-antigravity-byop-account-rotation.md create mode 100644 tests/unit/antigravity-byop-account-rotation.test.ts diff --git a/changelog.d/fixes/10470-antigravity-byop-account-rotation.md b/changelog.d/fixes/10470-antigravity-byop-account-rotation.md new file mode 100644 index 0000000000..9ec58e152a --- /dev/null +++ b/changelog.d/fixes/10470-antigravity-byop-account-rotation.md @@ -0,0 +1 @@ +- **fix(antigravity):** automatically rotate to a sibling account when one is BYOP (GCP Project ID required, `gcp_project_required` 422) — the account is excluded from selection for 24h and the request succeeds via another account instead of failing fast; the actionable 422 is surfaced only when no sibling exists (follow-up to the #10424 BYOP fast-fail) ([#10470](https://github.com/diegosouzapw/OmniRoute/pull/10470)) — thanks @rqzbeh diff --git a/open-sse/config/errorConfig.ts b/open-sse/config/errorConfig.ts index b326af7378..7e7355948f 100644 --- a/open-sse/config/errorConfig.ts +++ b/open-sse/config/errorConfig.ts @@ -81,6 +81,10 @@ export const COOLDOWN_MS = { // account, so re-probe only after a long window (or when the operator routes // egress through a supported-region proxy). geoBlocked: 24 * 60 * 60 * 1000, + // Antigravity BYOP (GCP_PROJECT_REQUIRED): nothing changes on the account + // until the operator enters a Project ID, so keep the connection excluded + // from selection for a long window (mirrors the geo-blocked treatment). + gcpProjectRequired: 24 * 60 * 60 * 1000, }; /** diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 3555467a40..e955ffd5d3 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2940,7 +2940,18 @@ export async function handleChatCore({ ? (extractSessionAffinityKey(body, clientRawRequest?.headers) ?? null) : null; - while (attempts < maxAttempts) { + // ── Antigravity BYOP 422 account-rotation state ───────────────────── + // A GCP_PROJECT_REQUIRED 422 is account-specific (that Google + // account lacks a GCP Project ID). Rotate to a sibling antigravity + // account instead of surfacing the error, so multi-account setups + // keep working without user action. Tracked separately from + // maxAttempts so non-BYOP antigravity failures never get a second + // shot (no double upstream calls). + const antigravityByopExcludedIds: string[] = []; + let antigravityByopRotationPending = false; + + while (attempts < maxAttempts || antigravityByopRotationPending) { + antigravityByopRotationPending = false; // consumed per iteration trace("pre_executor", { attempt: attempts }); updatePendingScope(pendingScope, { stage: "sending_to_provider", @@ -3187,6 +3198,55 @@ export async function handleChatCore({ continue; } + // ── Antigravity BYOP 422 account rotation ─────────────────────── + // GCP_PROJECT_REQUIRED (422, code gcp_project_required) means + // THIS Google account must Bring Its Own GCP Project. Mark the + // connection excluded (rateLimitedUntil, best-effort) and rotate + // to a sibling antigravity account so the request succeeds + // without user action. When no sibling exists (or all are BYOP), + // fall through: the error-state block excludes the connection + // and the actionable 422 is surfaced. + if (provider === "antigravity" && res.response.status === 422) { + const byopBody = await res.response + .clone() + .text() + .catch(() => ""); + if (byopBody.includes("gcp_project_required")) { + const byopFailedId = + executionConnectionId || credentials?.connectionId || connectionId; + if (byopFailedId) { + if (!antigravityByopExcludedIds.includes(String(byopFailedId))) { + antigravityByopExcludedIds.push(String(byopFailedId)); + } + try { + const { setConnectionRateLimitUntil } = await import("@/lib/db/providers"); + setConnectionRateLimitUntil( + String(byopFailedId), + Date.now() + COOLDOWN_MS.gcpProjectRequired + ); + } catch { + // best-effort — never break the rotation path + } + } + const byopNextCreds = await getProviderCredentials( + "antigravity", + null, + null, + modelToCall || model || requestedModel || null, + { excludeConnectionIds: [...antigravityByopExcludedIds] } + ).catch(() => null); + if (byopNextCreds && !byopNextCreds.allRateLimited) { + log?.warn?.( + "ANTIGRAVITY_BYOP_ROTATION", + `BYOP 422 on connection ${String(byopFailedId).slice(0, 8)} → rotating to ${String(byopNextCreds.connectionId).slice(0, 8)}` + ); + Object.assign(credentials, byopNextCreds); + antigravityByopRotationPending = true; + continue; + } + } + } + // For streaming: release the semaphore when the client drains or cancels the stream. if (stream) { const originalBody = res.response.body; @@ -4195,6 +4255,27 @@ export async function handleChatCore({ console.warn( `[provider] Node ${errorConnectionId} geo-blocked (${statusCode}) — excluded for ${Math.ceil(geoCooldownMs / 1000)}s, trying other accounts` ); + } else if (errorType === PROVIDER_ERROR_TYPES.GCP_PROJECT_REQUIRED) { + // Antigravity BYOP: the account must Bring Its Own GCP Project. + // Account-specific and fixable by entering a Project ID — never a + // model lockout, never a ban. Exclude the connection for the + // cooldown window so selection prefers sibling accounts; the 422 + // body carries the actionable message when no sibling is available. + const byopCooldownMs = COOLDOWN_MS.gcpProjectRequired ?? 24 * 60 * 60 * 1000; + await updateProviderConnection(errorConnectionId, { + lastErrorType: errorType, + lastError: message, + errorCode: statusCode, + }); + try { + const { setConnectionRateLimitUntil } = await import("@/lib/db/providers"); + setConnectionRateLimitUntil(errorConnectionId, Date.now() + byopCooldownMs); + } catch { + // best-effort — never break the error path + } + console.warn( + `[provider] Node ${errorConnectionId} GCP project required (${statusCode}) — excluded for ${Math.ceil(byopCooldownMs / 1000)}s, routing to other accounts (enter a Project ID to restore)` + ); } else if (errorType === PROVIDER_ERROR_TYPES.MODEL_NOT_FOUND) { // 404 — model/endpoint does not exist upstream. Lock the model so the // retry/backoff loop stops hammering the dead endpoint (which would diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index 43c1aa3079..735f44eb08 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -80,6 +80,10 @@ export const PROVIDER_ERROR_TYPES = { MODEL_NOT_FOUND: "model_not_found", FINGERPRINT_REJECTION: "fingerprint_rejection", GEO_BLOCKED: "geo_blocked", + // Antigravity BYOP fast-fail (executor 422, code gcp_project_required): the + // Google account must Bring Its Own GCP Project. Account-specific and + // fixable by entering a Project ID — never a model lockout and never a ban. + GCP_PROJECT_REQUIRED: "gcp_project_required", }; export const CONTEXT_OVERFLOW_SIGNALS = [ @@ -385,6 +389,15 @@ export function classifyProviderError( } if (statusCode >= 500) return PROVIDER_ERROR_TYPES.SERVER_ERROR; + // Antigravity BYOP fast-fail (executor emits 422 with code + // gcp_project_required when the Google account must Bring Its Own GCP + // Project). Account-specific and fixable by entering a Project ID in the + // dashboard — classified separately so chatCore rotates to sibling accounts + // and excludes the connection instead of locking the model or banning it. + if (statusCode === 422 && bodyStr.includes("gcp_project_required")) { + return PROVIDER_ERROR_TYPES.GCP_PROJECT_REQUIRED; + } + if (statusCode === 400) { if (isContextOverflow(bodyStr)) { return PROVIDER_ERROR_TYPES.CONTEXT_OVERFLOW; diff --git a/tests/unit/antigravity-byop-account-rotation.test.ts b/tests/unit/antigravity-byop-account-rotation.test.ts new file mode 100644 index 0000000000..cba1318d16 --- /dev/null +++ b/tests/unit/antigravity-byop-account-rotation.test.ts @@ -0,0 +1,253 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts"; + +// Regression guard for the Antigravity BYOP account-rotation follow-up: +// a GCP_PROJECT_REQUIRED 422 is account-specific (that Google account lacks +// a GCP Project ID), so chatCore must mark the account excluded and rotate +// to a sibling antigravity account instead of surfacing the error. When no +// sibling exists, the actionable 422 fast-fail is surfaced and the connection +// is excluded so selection prefers any other account. +const harness = await createChatPipelineHarness("antigravity-byop-rotation"); +const { BaseExecutor, buildRequest, handleChat, resetStorage, settingsDb } = harness; +const providersDb = await import("../../src/lib/db/providers.ts"); +const { clearAntigravityProjectCache } = + await import("../../open-sse/services/antigravityProjectBootstrap.ts"); +const { seedAntigravityIdeVersionCache, seedAntigravityCliVersionCache } = + await import("../../open-sse/services/antigravityVersion.ts"); + +test.beforeEach(async () => { + BaseExecutor.RETRY_CONFIG.delayMs = 0; + process.env.ANTIGRAVITY_CREDITS = "off"; + await resetStorage(); + await settingsDb.updateSettings({ requestRetry: 0, maxRetryIntervalSec: 0 }); + clearAntigravityProjectCache(); + seedAntigravityIdeVersionCache("2026.04.17-byop-rotation-test"); + seedAntigravityCliVersionCache("2026.04.17-byop-rotation-test"); +}); + +test.afterEach(() => { + clearAntigravityProjectCache(); + delete process.env.ANTIGRAVITY_CREDITS; +}); + +test.after(async () => { + await harness.cleanup(); +}); + +async function createAntigravityAccount(options: { + name: string; + email: string; + accessToken: string; + refreshToken: string; + priority?: number; +}) { + const connection = await providersDb.createProviderConnection({ + provider: "antigravity", + authType: "oauth", + name: options.name, + email: options.email, + accessToken: options.accessToken, + refreshToken: options.refreshToken, + expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + providerSpecificData: {}, + isActive: true, + testStatus: "active", + priority: options.priority, + }); + assert(connection && typeof connection.id === "string"); + return connection; +} + +test("Antigravity BYOP 422 rotates to a sibling account and the request succeeds", async () => { + const byopAccount = await createAntigravityAccount({ + name: "antigravity-byop-a", + email: "byop-a@example.test", + accessToken: "fake-byop-account-a-token", + refreshToken: "fake-byop-account-a-refresh", + priority: 1, // selected first — forces the rotation path + }); + const healthyAccount = await createAntigravityAccount({ + name: "antigravity-healthy-b", + email: "byop-b@example.test", + accessToken: "fake-healthy-account-b-token", + refreshToken: "fake-healthy-account-b-refresh", + priority: 2, + }); + + let onboardCallsForA = 0; + const modelCalls: Array<{ token: string }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init); + if (request.url.startsWith("https://oauth2.googleapis.com/token")) { + // Antigravity OAuth refreshes mid-flow; echo each account's own token so + // the executor keeps using the per-account identity below. + const form = await request.text().catch(() => ""); + const refreshMatch = form.match(/refresh_token=([^&]+)/); + const refreshToken = refreshMatch ? decodeURIComponent(refreshMatch[1]) : ""; + const accessToken = + refreshToken === "fake-byop-account-a-refresh" + ? "fake-byop-account-a-token" + : "fake-healthy-account-b-token"; + return new Response(JSON.stringify({ access_token: accessToken, expires_in: 3600 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + if (request.url.endsWith(":loadCodeAssist")) { + const token = (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, ""); + if (token === "fake-healthy-account-b-token") { + // Sibling account owns a Cloud Code project — discovery succeeds. + return new Response( + JSON.stringify({ cloudaicompanionProject: "projects/healthy-b-project" }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + // BYOP account: empty discovery — forces the onboardUser path. + return new Response("{}", { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + if (request.url.endsWith(":onboardUser")) { + const token = (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, ""); + if (token === "fake-byop-account-a-token") { + onboardCallsForA += 1; + // 200 done WITHOUT cloudaicompanionProject → Google BYOP (tracked #8491). + return new Response(JSON.stringify({ done: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + // Healthy account: onboarding creates the project. + return new Response( + JSON.stringify({ + done: true, + cloudaicompanionProject: { name: "projects/healthy-b-project" }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + if (request.url.includes("cloudcode-pa.googleapis.com")) { + modelCalls.push({ + token: (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, ""), + }); + // The executor always uses the SSE endpoint (streamGenerateContent?alt=sse), + // even for non-streaming requests. + return new Response( + 'data: {"response":{"candidates":[{"content":{"parts":[{"text":"ok from account B"}]},"finishReason":"STOP"}]}}\n\n', + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ); + } + throw new Error(`Unexpected external fetch: ${request.url}`); + }; + + try { + const response = await handleChat( + buildRequest({ + body: { + model: "antigravity/gemini-2.5-flash", + stream: false, + messages: [{ role: "user", content: "hello" }], + }, + }) + ); + + assert.equal(response.status, 200); + const bodyText = await response.text().catch(() => ""); + assert.match(bodyText, /ok from account B/); + + // The model call must have gone out with the SIBLING account's token. + assert.ok(modelCalls.length >= 1, "model call should have been made"); + assert.equal(modelCalls[0].token, "fake-healthy-account-b-token"); + + // BYOP detection ran exactly once for account A (cached per token). + assert.equal(onboardCallsForA, 1); + + // Account A is excluded (rateLimitedUntil set in the future). + const updatedA = await providersDb.getProviderConnectionById(byopAccount.id); + assert.ok( + updatedA && Number(updatedA.rateLimitedUntil) > Date.now(), + "BYOP account should be excluded from selection" + ); + // Account B must NOT be excluded. + const updatedB = await providersDb.getProviderConnectionById(healthyAccount.id); + assert.ok( + !updatedB || + !Number(updatedB.rateLimitedUntil) || + Number(updatedB.rateLimitedUntil) <= Date.now(), + "healthy sibling account must not be excluded" + ); + } finally { + globalThis.fetch = originalFetch; + clearAntigravityProjectCache(); + } +}); + +test("Antigravity BYOP with no sibling account surfaces the actionable 422 and excludes the connection", async () => { + const byopAccount = await createAntigravityAccount({ + name: "antigravity-byop-only", + email: "byop-only@example.test", + accessToken: "fake-byop-only-token", + refreshToken: "fake-byop-only-refresh", + priority: 1, + }); + + const originalFetch = globalThis.fetch; + globalThis.fetch = async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init); + if (request.url.startsWith("https://oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ access_token: "fake-byop-only-token", expires_in: 3600 }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + if (request.url.endsWith(":loadCodeAssist")) { + return new Response("{}", { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + if (request.url.endsWith(":onboardUser")) { + // BYOP: 200 done WITHOUT cloudaicompanionProject. + return new Response(JSON.stringify({ done: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + throw new Error(`Unexpected external fetch: ${request.url}`); + }; + + try { + const response = await handleChat( + buildRequest({ + body: { + model: "antigravity/gemini-2.5-flash", + stream: false, + messages: [{ role: "user", content: "hello" }], + }, + }) + ); + const payload = (await response.json()) as { + error?: { code?: string; message?: string }; + }; + + assert.equal(response.status, 422); + // chatCore's error formatter rebuilds the body, so the code may be + // generic — the actionable message must survive (same assertion as the + // existing BYOP chat test). + assert.match(String(payload.error?.message), /GCP_PROJECT_REQUIRED/); + + // No sibling exists, so the connection is excluded for future requests. + const updated = await providersDb.getProviderConnectionById(byopAccount.id); + assert.ok( + updated && Number(updated.rateLimitedUntil) > Date.now(), + "BYOP account should be excluded from selection" + ); + } finally { + globalThis.fetch = originalFetch; + clearAntigravityProjectCache(); + } +}); diff --git a/tests/unit/error-classifier.test.ts b/tests/unit/error-classifier.test.ts index d9259f5355..c4b7e8b2d4 100644 --- a/tests/unit/error-classifier.test.ts +++ b/tests/unit/error-classifier.test.ts @@ -368,3 +368,36 @@ test("isCloudflareFingerprintRejection: space-separated and URL-path forms match "URL path" ); }); + +test("classifyProviderError: 422 + gcp_project_required => GCP_PROJECT_REQUIRED (BYOP fast-fail)", () => { + const body = JSON.stringify({ + error: { + message: + "GCP_PROJECT_REQUIRED: Google Antigravity now requires a free GCP Project ID. " + + "Create one at console.cloud.google.com and enter it in Providers → Antigravity.", + type: "gcp_project_required", + code: "gcp_project_required", + }, + }); + assert.equal( + classifyProviderError(422, body, "antigravity"), + PROVIDER_ERROR_TYPES.GCP_PROJECT_REQUIRED + ); +}); + +test("classifyProviderError: 422 without the BYOP code stays unclassified (no model lockout)", () => { + // The sibling missing-project error (code missing_project_id) and any other + // 422 must NOT map to GCP_PROJECT_REQUIRED — and never to MODEL_NOT_FOUND, + // so chatCore keeps its fail-closed behavior without locking the model. + assert.equal( + classifyProviderError( + 422, + JSON.stringify({ + error: { code: "missing_project_id", message: "Missing Google projectId" }, + }), + "antigravity" + ), + null + ); + assert.equal(classifyProviderError(422, "some other body", "antigravity"), null); +}); From d87b97a78635725139fbff234905d1819c1db268 Mon Sep 17 00:00:00 2001 From: 3g0r1ch Date: Thu, 20 Aug 2026 23:28:30 +0300 Subject: [PATCH 109/135] =?UTF-8?q?feat(routing):=20adaptive=20feedback=20?= =?UTF-8?q?loop=20v2=20=E2=80=94=20operational/semantic=20quality,=20confi?= =?UTF-8?q?dence,=20TTFT/ITL,=20end-to-end=20test=20(#10881)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — feature substancial e bem estruturada: separa qualidade operacional (comportamento de wire: 4xx/5xx, 429, respostas malformadas, stream interrompido) de qualidade semântica (só setada por avaliadores externos, nunca inferida do sucesso HTTP), com confidence/sample-awareness para não deixar poucos sucessos de sorte dominarem o ranking. Instrumentação de streaming (TTFT/ITL) threaded até RoutingEvent, endpoint de explicabilidade, e teste E2E determinístico cobrindo degradação→recuperação→blip. Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - 59/59 testes passando (mlx-provider, routing-adaptive-e2e, routing-events(-concurrency), routing-otel, routing-quality, routing-scoring-quality, stream-timing, auto-combo-scoring-clamp) --- docs/architecture/ADAPTIVE_ROUTING.md | 350 ++++++++++++++++++ docs/architecture/meta.json | 3 +- docs/getting-started/PROVIDERS-GUIDE.md | 90 +++++ docs/reference/PROVIDER_REFERENCE.md | 4 +- open-sse/config/providers/index.ts | 4 + .../config/providers/registry/mlx/index.ts | 66 ++++ open-sse/handlers/chatCore.ts | 139 +++++++ open-sse/services/autoCombo/scoring.ts | 28 +- open-sse/services/combo.ts | 4 + open-sse/services/combo/quotaShareStrategy.ts | 4 +- open-sse/services/routing/events.ts | 220 +++++++++++ open-sse/services/routing/index.ts | 133 +++++++ open-sse/services/routing/otel.ts | 227 ++++++++++++ open-sse/services/routing/quality.ts | 313 ++++++++++++++++ open-sse/utils/stream.ts | 70 +++- open-sse/utils/streamTiming.ts | 83 +++++ package.json | 1 + scripts/perf/routing-events-bench.ts | 175 +++++++++ src/app/api/v1/explain/routing/route.ts | 72 ++++ src/lib/usage/comboScoringInspector.ts | 24 +- src/shared/constants/providers.ts | 4 + src/shared/constants/providers/local.ts | 26 ++ src/shared/types/utilization.ts | 3 +- tests/unit/auto-combo-scoring-clamp.test.ts | 1 + tests/unit/mlx-provider.test.ts | 57 +++ tests/unit/routing-adaptive-e2e.test.ts | 199 ++++++++++ tests/unit/routing-events-concurrency.test.ts | 172 +++++++++ tests/unit/routing-events.test.ts | 141 +++++++ tests/unit/routing-otel.test.ts | 129 +++++++ tests/unit/routing-quality.test.ts | 187 ++++++++++ tests/unit/routing-scoring-quality.test.ts | 96 +++++ tests/unit/stream-timing.test.ts | 86 +++++ 32 files changed, 3078 insertions(+), 33 deletions(-) create mode 100644 docs/architecture/ADAPTIVE_ROUTING.md create mode 100644 open-sse/config/providers/registry/mlx/index.ts create mode 100644 open-sse/services/routing/events.ts create mode 100644 open-sse/services/routing/index.ts create mode 100644 open-sse/services/routing/otel.ts create mode 100644 open-sse/services/routing/quality.ts create mode 100644 open-sse/utils/streamTiming.ts create mode 100644 scripts/perf/routing-events-bench.ts create mode 100644 src/app/api/v1/explain/routing/route.ts create mode 100644 tests/unit/mlx-provider.test.ts create mode 100644 tests/unit/routing-adaptive-e2e.test.ts create mode 100644 tests/unit/routing-events-concurrency.test.ts create mode 100644 tests/unit/routing-events.test.ts create mode 100644 tests/unit/routing-otel.test.ts create mode 100644 tests/unit/routing-quality.test.ts create mode 100644 tests/unit/routing-scoring-quality.test.ts create mode 100644 tests/unit/stream-timing.test.ts diff --git a/docs/architecture/ADAPTIVE_ROUTING.md b/docs/architecture/ADAPTIVE_ROUTING.md new file mode 100644 index 0000000000..73bacfec3a --- /dev/null +++ b/docs/architecture/ADAPTIVE_ROUTING.md @@ -0,0 +1,350 @@ +--- +title: "Adaptive Routing: Routing Events, Quality Feedback & Explainability" +version: 3.8.50 +lastUpdated: 2026-08-20 +--- + +# Adaptive Routing: Routing Events, Quality Feedback & Explainability + +This document describes the feedback-driven adaptive routing foundation added to +OmniRoute. It is deliberately small: it introduces a typed routing-outcome +channel, an online quality signal that feeds the existing auto-combo scorer, an +optional OpenTelemetry exporter, and an explainability endpoint. It does **not** +replace the existing resilience stack (circuit breaker, connection cooldown, +model lockout, health matrix, autopilot) — it complements it. + +## 1. Architectural context + +OmniRoute is a data plane with a **request hot path** and a **control/intelligence +plane**. The hot path must stay fast, memory-efficient, asynchronous, resilient and +predictable. Evaluation, quality scoring, experiments and historical analysis belong +to the control plane. + +``` +AI Agent / IDE + │ + ▼ +┌─────────────────────┐ +│ OmniRoute │ data plane (fast, sync, in-memory) +│ routing / failover │ +│ health / guardrail │ +│ cache / streaming │ +└──────────┬──────────┘ + │ RoutingEvent (fire-and-forget, ~0.2µs) + ▼ +┌─────────────────────┐ +│ Feedback sinks │ control plane (async, best-effort) +│ quality tracker │ +│ OTel exporter │ +│ explain store │ +└──────────┬──────────┘ + ▼ quality score + auto-combo scorer +``` + +### What was already there (audited, not duplicated) + +| Concept | Existing implementation | +| ----------------------------------- | -------------------------------------------------------------------------------------------------- | +| Availability (can we send traffic?) | Circuit breaker (CLOSED/DEGRADED/OPEN/HALF_OPEN, DB-persisted), connection cooldown, model lockout | +| Health reporting | `providerHealthMatrix.ts`, `providerHealthAutopilot.ts` | +| Shadow traffic | `open-sse/services/combo/shadowRouting.ts` | +| Guardrails | `src/lib/guardrails/` (pre/post hooks) | +| Exact cache | `src/lib/semanticCache.ts` (signature-based) | +| Evaluators / eval-driven routing | `src/lib/evals/`, `open-sse/services/evalRouting.ts` | +| Combo decision explainability | `open-sse/services/combo/decisionTrace.ts` | +| Dashboard real-time events | `src/lib/events/eventBus.ts` (UI notification channel, `unknown` payloads, 100-entry history) | + +The routing-event layer is **not** a re-implementation of `eventBus`: that bus is +the dashboard's real-time notification channel (typed _event names_, opaque +payloads, UI consumers). `RoutingEvent` is a typed _outcome_ struct +(latency/tokens/cost/outcome/finish-reason) consumed by the control plane's +feedback sinks (quality tracker, OTel exporter, explain store). + +### What was missing (added here) + +1. A **typed routing-outcome event + sink abstraction** (`RoutingEvent` / + `RoutingEventSink`). `decisionTrace` is combo-scoped and in-memory-only; + `comboMetrics` are cumulative counters; `call_logs` is raw async persistence. + None is a typed, sink-based outcome channel that a quality tracker, an OTel + exporter, or a Future-AGI-style evaluator can subscribe to. +2. An **online quality signal** (EWMA) for output quality — the scorer previously + proxied "quality" only through static task fitness and opt-in eval pass-rates. +3. An **optional, dependency-free OTel exporter** using GenAI semantic conventions. +4. An **explainability endpoint** returning the real routing decisions + quality state. + +## 2. Routing Events (feedback foundation) + +Files: `open-sse/services/routing/events.ts`, `.../index.ts` + +A `RoutingEvent` carries only routing metadata: + +```ts +interface RoutingEvent { + requestId: string; + provider: string; + model: string; + strategy: string; // "auto" | "priority" | "direct" | ... + latencyMs: number; + ttftMs: number | null; + inputTokens: number | null; + outputTokens: number | null; + cost: number | null; + retries: number; + fallbackUsed: boolean; + outcome: RoutingOutcome; // allowlisted union + status: number | null; + finishReason: string | null; + connectionId: string | null; + ts: number; +} +``` + +`RoutingEventSink` is a `Send+Sync`-style trait in TypeScript: + +```ts +interface RoutingEventSink { + readonly name: string; + record(event: RoutingEvent): void; // must be O(1), no sync I/O +} +``` + +The hot path calls `emitRoutingEvent(event)` once per completed request +(the streaming-completion callback, the non-streaming success path, and the +malformed-200 failure path in `handleChatCore`). Dispatch is synchronous fan-out +to registered sinks, but each sink only enqueues/updates in-memory state. **No +synchronous database writes, no network I/O on the hot path.** + +Default sinks: + +- `MemoryRoutingEventStore` — bounded (500) ring buffer, newest-first, for the + explain endpoint. +- `QualityTracker` consumer — updates the EWMA quality estimate. +- `OtlpHttpsEventSink` — optional, enabled only when `OMNIROUTE_OTEL_ENDPOINT` + (or `OTEL_EXPORTER_OTLP_ENDPOINT`) is set. + +### Measured overhead (honest comparison) + +`npm run bench:routing-events` on this workstation (100k iterations; sub-µs ops +measured as aggregate µs/op because per-op percentiles are below +`performance.now()` timer resolution): + +| Scenario | µs/op | ops/s | +| --------------------------------- | ------ | ------ | +| baseline (scoring only) | ~0.045 | ~22 M | +| baseline + RoutingEvent (2 sinks) | ~0.168 | ~5.9 M | +| baseline + event + OTel enqueue | ~0.163 | ~6.1 M | +| concurrent (8 interleaved bursts) | ~0.18 | — | + +The event-dispatch delta over baseline scoring is ~0.12 µs/request; the OTel sink +only enqueues (O(1) buffer push), adding nothing measurable. These numbers are +machine-specific and relative — not a production guarantee. The v1 "~0.2 µs" +figure was an aggregate estimate; this methodology separates the scoring baseline +from the event-dispatch cost. + +## 3. Quality Signal (feedback-driven provider state) + +Files: `open-sse/services/routing/quality.ts` + +v2 separates **operational** from **semantic** quality: + +- **Operational** — derived from the routing hot path (HTTP 4xx/5xx, connection + failures, 429s, malformed responses, stream interruptions, `finish_reason=length`, + zero-output successes, latency/TTFT EWMA). A 200 is NOT treated as semantic + quality. +- **Semantic** — the actual value of the generated output. ONLY ever produced by + an evaluator via `setSemanticQuality()`. It is `null` until one provides it and + never leaks into the operational score. + +Per-(provider, model) state (EWMA + bounded counters): + +- `successEwma` — EWMA (α=0.2) of outcome success. +- `latencyEwma` / `ttftEwma` — EWMA of latency (α=0.1). +- `samples`, `anomalies`, `rateLimited`, `semantic`, `semanticConfidence`. +- `recencyMs` — how recently the model was last observed. + +### Confidence / sample awareness + +`confidence = clamp01(samples / 50)`, and the score returned to the scorer is +blended toward the neutral midpoint: + +``` +score = 0.5 + confidence * (operational - 0.5) +``` + +Consequences (verified by tests): + +- A cold provider (0 samples) scores **0.5** — not unfairly penalized, but + unable to dominate a provider with thousands of solid observations. +- A provider with 7 lucky successes is pulled toward 0.5 (never dominates from + optimistic initialization). +- A provider with 50+ samples converges to its true operational score. +- Degradation and recovery are gradual (EWMA), and one isolated failure does + not destroy a healthy provider. + +`ProviderQuality` exposes `{ operational, semantic, confidence, samples, anomalies, +rateLimited, successEwma, latencyEwmaMs, ttftEwmaMs, recencyMs }`. + +This feeds the auto-combo scorer as the `quality` scoring factor: + +- `ScoringFactors.quality` / `ScoringWeights.quality` in + `open-sse/services/autoCombo/scoring.ts`. +- `DEFAULT_WEIGHTS`: `health` 0.1905 → 0.1605, `quality` 0.03. Sum stays 1.0. +- `buildAutoCandidates` populates `candidate.quality` from the tracker; candidates + without data default to neutral **0.5** (a cold candidate is neither boosted nor + penalized). + +The closed loop: + +``` +RoutingEvent → QualityTracker → getQualityScore → auto-combo quality factor + ↑ │ + └────── request outcome (handleChatCore) ←────────────┘ +``` + +### Hard exclusion vs soft penalty + +The quality signal is a **soft adaptive preference** only. Hard exclusion stays +with the existing resilience stack: circuit breaker OPEN, quota exhausted, +auth failure, model lockout — none of these are affected by the quality score. +A provider whose quality score dips temporarily is de-preferenced, never +hard-disabled. + +## 3b. Canonical stream timing (TTFT / ITL) + +Files: `open-sse/utils/streamTiming.ts` + +`createStreamTiming()` is the single instrumentation seam for the streaming path, +wired into `createSSEStream` (open-sse/utils/stream.ts): + +- `markByte()` — first upstream chunk received. +- `markForward()` — first chunk forwarded to the client (used for TTFT). +- `markInterrupted()` — stream timeout/abort/error before a clean finish. +- `ttft()` = first-forwarded-SSE-chunk latency. **This is NOT token-level TTFT** — + a single SSE chunk may carry zero/one/many tokens. Documented precisely. +- `avgItlMs()` = mean inter-chunk gap (a chunk-latency proxy for ITL). + +TTFT/ITL/interrupted flow into the `RoutingEvent` (`ttftMs`, `itlMs`) and are +exported as GenAI/OmniRoute span attributes by the OTel sink. + +## 4. OpenTelemetry / GenAI observability + +Files: `open-sse/services/routing/otel.ts` + +- Dependency-free OTLP/HTTP JSON exporter (uses global `fetch`, no + `@opentelemetry/*` SDK). +- Spans follow GenAI semantic conventions (`gen_ai.provider.name`, + `gen_ai.request.model`, `gen_ai.usage.input_tokens/output_tokens`, + `gen_ai.completion.finish_reason`, `gen_ai.system`) plus OmniRoute routing + attributes (outcome, status, ttft, retries, fallback). +- `record()` only enqueues into a bounded buffer (O(1)); a background timer + flushes via `POST {endpoint}/v1/traces` asynchronously. Under overload the + oldest events are dropped (`dropped` counter) — never backpressure the data + plane. +- **Disabled unless configured.** `OMNIROUTE_OTEL_ENDPOINT` (or + `OTEL_EXPORTER_OTLP_ENDPOINT`) must be set; otherwise the sink is not + registered and zero OTel code runs. + +## 5. Explainability + +- `GET /v1/explain/routing` returns the recent `RoutingEvent`s (the real + decisions, newest first) and the per-provider/model quality snapshot. +- Auth mirrors `/v1/combos` (Bearer API key or dashboard session; anonymous on + single-user local deployments with `REQUIRE_API_KEY=false`). +- Combo-level per-invocation traces remain available via the existing + `decisionTrace.ts` (header `X-OmniRoute-Combo-Trace`). +- Safety: events carry only routing metadata, never prompts/bodies/credentials. + +## 6. Evaluation-plane integration (Future AGI readiness) + +OmniRoute treats Future AGI (or any evaluator) as a **potential +intelligence/evaluation backend, not a dependency**. The seams: + +- A `RoutingEventSink` can forward events to an evaluator asynchronously. +- The `MemoryRoutingEventStore` + quality snapshot give an evaluator the raw + decision stream. +- A future `Evaluator` (deterministic, local judge, HTTP, WASM) would consume + events/traces and return a `QualityScore` that feeds the same + `getQualityScore`/quality-factor path. +- Existing eval-driven routing (`open-sse/services/evalRouting.ts`) already + re-orders combo targets by `eval_runs` pass-rates when enabled. + +No evaluation runs synchronously on the request path, and the gateway operates +fully with the evaluator absent. + +## 7. Final architectural review + +1. **What remains on the synchronous hot path?** Routing/scoring, guardrail + pre-checks, cache lookup, and one `emitRoutingEvent` fan-out (~0.12 µs over + baseline scoring) to in-memory sinks. +2. **What moved to asynchronous processing?** OTel export (timer + fetch), + `call_logs`/usage persistence, semantic-cache writes, quality is in-memory + and O(1) (no async needed). +3. **How does a routing outcome become feedback?** `handleChatCore` emits a + `RoutingEvent` → `QualityTracker` updates EWMA state → `getQualityScore` + feeds the auto-combo `quality` factor. +4. **How does quality influence future routing?** A low quality score reduces + the weighted score of that provider/model in `scoreAutoTargets`, so degraded + models are gradually de-preferenced and recover as their EWMA improves. +5. **How can Future AGI integrate without becoming a dependency?** Via the + `RoutingEventSink` interface / a future `Evaluator` adapter — no hardcoded + dependency. +6. **What happens when the evaluator is unavailable?** Routing is unaffected; + quality falls back to neutral (1.0) for models with no observed signal. +7. **What happens when telemetry is unavailable?** The OTel sink simply isn't + registered; the rest of the routing layer runs unchanged. +8. **What happens under overload?** The OTel buffer drops oldest events; quality + and the ring buffer are bounded by construction; no backpressure. +9. **How does provider state recover after degradation?** EWMA re-converges as + successes accumulate; warmup keeps cold models neutral; the circuit breaker + independently recovers via HALF_OPEN probes. +10. **Which proposed features were intentionally NOT implemented, and why?** + - Shadow traffic / experiments — already implemented + (`combo/shadowRouting.ts`); not re-built. + - Guardrails — already implemented (`src/lib/guardrails/`); not duplicated. + - Semantic cache — already implemented (`src/lib/semanticCache.ts`); not + duplicated. + - A full experiment-management platform, dataset tooling, prompt-optimization + platform, vector DB, or mandatory external OTel infrastructure — out of + scope for a lean data plane. + - A Rust `RoutingEvent` struct — the data plane is TypeScript; the TS type + is the adapted equivalent. + +## 8. Configuration reference + +| Variable | Default | Effect | +| ----------------------------- | ----------- | ------------------------------------------------------------------------------- | +| `OMNIROUTE_OTEL_ENDPOINT` | unset | When set, enables the OTLP/HTTP traces exporter (e.g. `http://collector:4318`). | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | Fallback alias for the OTLP endpoint. | +| `OTEL_SERVICE_NAME` | `omniroute` | `service.name` resource attribute. | + +## 9. Tests + +- `tests/unit/routing-events.test.ts` — event normalization, status + classification, bounded ring buffer, sink fan-out + isolation. +- `tests/unit/routing-quality.test.ts` — EWMA warmup, failure/success recovery, + anomaly penalties, 429 transient handling, snapshot, reset. +- `tests/unit/routing-scoring-quality.test.ts` — weight integrity, neutral + default, quality factor ranking. +- `tests/unit/routing-otel.test.ts` — enable gating, GenAI span payload, async + flush, drop-under-overload. +- `tests/unit/routing-events-concurrency.test.ts` — thousands of events, ring + buffer boundedness, throwing-sink isolation, interleaved async bursts, + reset-during-inserts. +- `tests/unit/routing-adaptive-e2e.test.ts` — deterministic end-to-end loop via + the real `scoreAutoTargets` scorer: healthy → degrade → recover → blip, plus + cold-start and lucky-cold-provider scenarios. +- `tests/unit/stream-timing.test.ts` — TTFT (first-forwarded-chunk), ITL, + first-byte vs first-forward, interruption, malformed/empty chunk safety. + +## 10. Pre-existing issues status (Phase 18) + +| Issue | Status | Notes | +| ----------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `omniglyph` export mismatch | **FIXED (environmental)** | `node_modules` was out of sync with `package-lock.json` (installed 1.3.1 vs locked 1.4.0). Running `npm install omniglyph@1.4.0` restored the locked version; type errors dropped to 0. Manifests unchanged. | +| Stale `getKnownContextOverflow` tests | **KNOWN — not fixed** | `combo-context-overflow-compression-probe.test.ts` imports a function that no longer exists in `open-sse/services/combo.ts` (only comments reference it). Fixing requires re-implementing or re-writing those tests — unrelated architectural churn. | +| `combo-runtime-unit-concurrency.test.ts` DB isolation | **KNOWN — not fixed** | Test-harness SQLite-isolation assertion fails when run directly; fails identically on the base branch. | +| i18n `llm.txt` drift | **KNOWN — not fixed** | `docs/i18n/*/llm.txt` differ from root; pre-existing, blocks the docs-sync pre-commit gate. | + +Environmental vs code issues are kept distinct; no unrelated failures are hidden +behind changed test filters. diff --git a/docs/architecture/meta.json b/docs/architecture/meta.json index dba5872324..c5b10b7923 100644 --- a/docs/architecture/meta.json +++ b/docs/architecture/meta.json @@ -12,6 +12,7 @@ "ROUTER_BACKENDS", "admission-lanes", "cluster-decisions", - "persistence-backend-boundary" + "persistence-backend-boundary", + "ADAPTIVE_ROUTING" ] } diff --git a/docs/getting-started/PROVIDERS-GUIDE.md b/docs/getting-started/PROVIDERS-GUIDE.md index 65de3c63ea..d6d20906c5 100644 --- a/docs/getting-started/PROVIDERS-GUIDE.md +++ b/docs/getting-started/PROVIDERS-GUIDE.md @@ -52,6 +52,8 @@ safely retry only the failures after a partial result. - **Pollinations** — Free GPT-5, Claude, Gemini (no key needed) - **LongCat** — 10M tokens free (one-time grant, requires account + KYC) - **Cloudflare AI** — 50+ models, 10K neurons/day + - **MLX Gemma 26B** — Local Apple Silicon model (~38.5 tok/s, ~15.9GB RAM) + - **MLX Qwen 3.8 27B** — Local Apple Silicon model (~9.1 tok/s, ~13.1GB RAM) 4. Click **Connect** 5. Done! You now have free AI access. @@ -79,6 +81,94 @@ safely retry only the failures after a partial result. 5. Login with your account 6. Done! You now have access to your subscription models. +### Option D: Local MLX Models (Apple Silicon) + +For Apple Silicon Macs with unified memory, OmniRoute supports connecting to local MLX models running via `mlx-lm.server` as regular OpenAI-compatible local providers. + +#### Prerequisites + +- **Apple Silicon Mac** (M1/M2/M3/M4) with 24GB+ unified memory recommended +- **uv** package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh` +- **mlx-lm**: `uv pip install mlx-lm` + +#### Quick Start + +1. **Install dependencies**: + + ```bash + # Install uv if not already installed + curl -LsSf https://astral.sh/uv/install.sh | sh + + # Install mlx-lm + uv pip install mlx-lm + ``` + +2. **Start MLX servers manually** (in separate terminals): + + ```bash + # Terminal 1: Gemma 4 26B A4B IT-QAT (port 11435) + uv run mlx_lm.server --model mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned --port 11435 --host 127.0.0.1 + + # Terminal 2: Qwen 3.8 27B MLX Mixed (port 11436) + uv run mlx_lm.server --model maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw --port 11436 --host 127.0.0.1 + ``` + +3. **Connect in OmniRoute Dashboard**: + - Go to **Providers** → **Add Provider** + - Select **MLX Gemma 26B** or **MLX Qwen 3.8 27B** + - Click **Connect** (no API key needed) + +4. **Use with OpenCode**: + ```bash + # Configure OpenCode to use OmniRoute + opencode config set api.base_url http://localhost:20128/v1 + opencode config set api.key + + # Use MLX models + opencode run --model mlx-gemma/gemma-4-26b + opencode run --model mlx-qwen/qwen3.8-27b + ``` + +#### Memory Management + +**Important**: With 24GB unified memory, only **one large MLX model can run at a time**. + +- Gemma 26B: ~15.9GB peak memory +- Qwen 3.8 27B: ~13.1GB peak memory + +You must manage this manually: + +- Run only one MLX server at a time, or +- Run both on separate machines, or +- Stop one before starting the other + +OmniRoute does not automatically manage MLX server processes — it only routes requests to the OpenAI-compatible endpoints you configure. + +#### Tool Calling Support + +Both models support OpenAI-compatible tool calling. Test with: + +```bash +curl -X POST http://localhost:20128/v1/chat/completions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "model": "mlx-gemma/gemma-4-26b", + "messages": [{"role": "user", "content": "What is 2+2? Use the calculator tool."}], + "tools": [{"type": "function", "function": {"name": "calculator", "description": "Calculate", "parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}}}] + }' +``` + +#### Troubleshooting + +| Issue | Solution | +| ------------------ | ----------------------------------------------------------------------------- | +| Server won't start | Check `uv run mlx_lm.server --help` and verify model IDs | +| Out of memory | Ensure only one model runs; close other apps; check Activity Monitor | +| Connection refused | Verify server is running on correct port (11435/11436) | +| Slow responses | First request loads model into memory (~30-60s); subsequent requests are fast | +| Tool calling fails | Ensure model supports tools; check OmniRoute logs for translation errors | + --- ## Best Free Providers diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index f6b2568af5..e4f3c4ad58 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -355,7 +355,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `zerolimitai` | `zerolimitai` | ZeroLimitAI | API key, aggregator | [link](https://www.zerolimitai.com) | Temporary free trial is advertised, but official pages conflict between 3 and 7 days; a 100-calls/day claim is not treated as permanent. | | `zylo-api` | `zylo` | Zylo API | API key, aggregator | [link](https://zyloai.net) | Basic plan: 10 RPM, 7,200 requests/day and 200,000 tokens/day; limited to Basic text models. | -## Local Providers (12) +## Local Providers (14) | ID | Alias | Name | Tags | Website | Notes | |----|-------|------|------|---------|-------| @@ -365,6 +365,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `llama-cpp` | `llamacpp` | llama.cpp | Local, self-hosted | [link](https://github.com/ggml-org/llama.cpp) | API key optional (use any value, e.g. sk-no-key-required). Configure the llama-server OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). Note: if Llamafile is also installed, both default to port 8080 — run only one at a time or override the port. | | `llamafile` | `llamafile` | Llamafile | Local, self-hosted | [link](https://github.com/Mozilla-Ocho/llamafile) | API key optional. Configure the local Llamafile OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). | | `lm-studio` | `lmstudio` | LM Studio | Local, self-hosted | [link](https://lmstudio.ai) | API key optional. Configure the local LM Studio OpenAI-compatible base URL (default: http://localhost:1234/v1). | +| `mlx-gemma` | `mlx-gemma` | MLX Gemma 26B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11435. Requires `uv` and `mlx-lm` installed. Model: `mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned` (~15.9GB peak memory). | +| `mlx-qwen` | `mlx-qwen` | MLX Qwen 3.8 27B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11436. Requires `uv` and `mlx-lm` installed. Model: `maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw` (~13.1GB peak memory). | | `ollama-local` | `ollama` | Ollama | Local, self-hosted | [link](https://ollama.com) | No API key required. Ollama runs locally — configure its OpenAI-compatible base URL (default: http://localhost:11434/v1) and make sure Ollama is running before connecting. | | `oobabooga` | `ooba` | oobabooga | Local, self-hosted | [link](https://github.com/oobabooga/text-generation-webui) | API key optional. Configure the local oobabooga OpenAI-compatible base URL (default: http://localhost:5000/v1). | | `sdwebui` | `sdwebui` | SD WebUI | Local | [link](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | No API key required. Configure the local WebUI base URL (default: http://localhost:7860). | diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 34189e0d00..c098dbf29f 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -3,6 +3,8 @@ import { unorouterProvider } from "./registry/unorouter/index.ts"; import { aimlapiProvider } from "./registry/aimlapi/index.ts"; import { byteplusProvider } from "./registry/byteplus/index.ts"; +import { mlxGemmaProvider } from "./registry/mlx/index.ts"; +import { mlxQwenProvider } from "./registry/mlx/index.ts"; import { ollama_cloudProvider } from "./registry/ollama-cloud/index.ts"; import { syntheticProvider } from "./registry/synthetic/index.ts"; import { ideogramProvider } from "./registry/ideogram/index.ts"; @@ -264,6 +266,8 @@ import { helixmindProvider } from "./registry/helixmind/index.ts"; export const REGISTRY: Record = { aimlapi: aimlapiProvider, + "mlx-gemma": mlxGemmaProvider, + "mlx-qwen": mlxQwenProvider, "ollama-cloud": ollama_cloudProvider, synthetic: syntheticProvider, ideogram: ideogramProvider, diff --git a/open-sse/config/providers/registry/mlx/index.ts b/open-sse/config/providers/registry/mlx/index.ts new file mode 100644 index 0000000000..24d3e8b232 --- /dev/null +++ b/open-sse/config/providers/registry/mlx/index.ts @@ -0,0 +1,66 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +// MLX ports (deterministic, documented) +const MLX_GEMMA_PORT = 11435; +const MLX_QWEN_PORT = 11436; + +// ───────────────────────────────────────────────────────────────────────────── +// Memory-aware context windows for MLX models on 24GB unified memory. +// Based on verified peak memory: Gemma 26B ~15.9GB, Qwen 27B ~13.1GB. +// KV cache estimate: 2 * 2 * layers * kv_heads * head_dim * num_ctx bytes. +// Conservative context windows to leave headroom for OS/other processes. +export const MLX_DEFAULT_CONTEXT_LIMIT = 32768; + +const CONTEXT_GEMMA_26B = 8192; // 15.9GB weights + ~3.5GB KV @ 8k = ~19.4GB (safe for 24GB) +const CONTEXT_QWEN_27B = 8192; // 13.1GB weights + ~3.5GB KV @ 8k = ~16.6GB (safe for 24GB) + +// ───────────────────────────────────────────────────────────────────────────── +// MLX Gemma 26B Provider +// Model: mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned +// Verified speed: ~38.5 tok/s, peak memory: ~15.9 GB +export const mlxGemmaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "mlx-gemma", + alias: "mlx-gemma", + baseUrl: `http://localhost:${MLX_GEMMA_PORT}/v1`, + modelsUrl: `http://localhost:${MLX_GEMMA_PORT}/v1/models`, + passthroughModels: false, + defaultContextLength: MLX_DEFAULT_CONTEXT_LIMIT, + models: [ + { + id: "mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned", + name: "Gemma 4 26B A4B IT-QAT (MLX)", + toolCalling: true, + supportsVision: false, + supportsReasoning: false, + contextLength: CONTEXT_GEMMA_26B, + maxOutputTokens: 8192, + }, + ], + timeoutMs: 120000, // Longer timeout for model loading +}); + +// ───────────────────────────────────────────────────────────────────────────── +// MLX Qwen3.8 27B Provider +// Model: maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw +// Verified speed: ~9.1 tok/s, peak memory: ~13.1 GB +export const mlxQwenProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "mlx-qwen", + alias: "mlx-qwen", + baseUrl: `http://localhost:${MLX_QWEN_PORT}/v1`, + modelsUrl: `http://localhost:${MLX_QWEN_PORT}/v1/models`, + passthroughModels: false, + defaultContextLength: MLX_DEFAULT_CONTEXT_LIMIT, + models: [ + { + id: "maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw", + name: "Qwen 3.8 27B MLX Mixed 3.80bpw", + toolCalling: true, + supportsVision: false, + supportsReasoning: false, + contextLength: CONTEXT_QWEN_27B, + maxOutputTokens: 8192, + }, + ], + timeoutMs: 120000, // Longer timeout for model loading +}); diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index e955ffd5d3..fa6085ad19 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -34,6 +34,38 @@ import { storeStreamingSemanticCacheResponse } from "./chatCore/streamingSemanti import { assembleStreamingPipeline } from "./chatCore/streamingPipeline.ts"; import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts"; import { applyResponsesInputPolicy } from "../services/responsesInputPolicy.ts"; +import { + createRoutingEvent, + emitRoutingEvent, + outcomeFromStatus, +} from "../services/routing/index.ts"; + +/** + * Best-effort finish_reason extraction from a (possibly translated) response + * body for routing-event telemetry. Returns null when the shape is unknown. + */ +function routingFinishReason(body: unknown): string | null { + if (!body || typeof body !== "object") return null; + const record = body as Record; + const choices = record.choices; + if (Array.isArray(choices)) { + const first = choices[0]; + if (first && typeof first === "object") { + const fr = (first as Record).finish_reason; + if (typeof fr === "string") return fr; + } + } + const output = record.output; + if (Array.isArray(output)) { + for (const item of output) { + if (item && typeof item === "object") { + const fr = (item as Record).finish_reason; + if (typeof fr === "string") return fr; + } + } + } + return null; +} import { getHeaderValueCaseInsensitive, isNoMemoryRequested, @@ -5054,6 +5086,27 @@ export async function handleChatCore({ }); persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, "malformed_translated_response"); trackPendingRequest(model, provider, pendingConnId, false); + // Routing event (feedback foundation) — record the malformed outcome so + // the quality tracker de-prioritizes this model over time. + void emitRoutingEvent( + createRoutingEvent({ + requestId: traceId || pendingRequestId || "unknown", + provider: provider || "unknown", + model: model || "unknown", + strategy: isCombo ? (comboStrategy ?? "combo") : "direct", + latencyMs: Date.now() - startTime, + ttftMs: null, + inputTokens: null, + outputTokens: null, + cost: null, + retries: 0, + fallbackUsed: false, // combo-level fallback tracked by decisionTrace + outcome: "malformed", + status: HTTP_STATUS.BAD_GATEWAY, + finishReason: routingFinishReason(translatedResponse), + connectionId: credentials?.connectionId ?? null, + }) + ); return createErrorResult( HTTP_STATUS.BAD_GATEWAY, malformedMessage, @@ -5154,6 +5207,43 @@ export async function handleChatCore({ response: { status: 200, data: translatedResponse }, }); + // Routing event (feedback foundation) — fire-and-forget, cheap. + void emitRoutingEvent( + createRoutingEvent({ + requestId: traceId || pendingRequestId || "unknown", + provider: provider || "unknown", + model: model || "unknown", + strategy: isCombo ? (comboStrategy ?? "combo") : "direct", + latencyMs: Date.now() - startTime, + ttftMs: null, + inputTokens: + usage && typeof usage === "object" + ? (() => { + const promptTokens = (usage as Record).prompt_tokens; + return typeof promptTokens === "number" && Number.isFinite(promptTokens) + ? promptTokens + : null; + })() + : null, + outputTokens: + usage && typeof usage === "object" + ? (() => { + const completionTokens = (usage as Record).completion_tokens; + return typeof completionTokens === "number" && Number.isFinite(completionTokens) + ? completionTokens + : null; + })() + : null, + cost: Number.isFinite(estimatedCost) ? estimatedCost : null, + retries: 0, + fallbackUsed: false, // combo-level fallback tracked by decisionTrace + outcome: "success", + status: 200, + finishReason: routingFinishReason(translatedResponse), + connectionId: credentials?.connectionId ?? null, + }) + ); + return { success: true, response: buildNonStreamingJsonResponse(translatedResponse, responseHeaders), @@ -5274,6 +5364,8 @@ export async function handleChatCore({ error: streamError, errorCode: streamErrorCode, ttft, + itlMs: streamItlMs, + interrupted: streamInterrupted, }) => { const normalizedStreamStatus = streamStatus || 200; if (streamCompletionRecorded) return; @@ -5377,6 +5469,53 @@ export async function handleChatCore({ endpoint: endpointPath, }); + // Routing event (feedback foundation) — fire-and-forget, cheap, never blocks + // the stream. Feeds the quality tracker + optional OTel exporter. + void emitRoutingEvent( + createRoutingEvent({ + requestId: traceId || pendingRequestId || "unknown", + provider: provider || "unknown", + model: model || "unknown", + strategy: isCombo ? (comboStrategy ?? "combo") : "direct", + latencyMs: Date.now() - startTime, + ttftMs: typeof ttft === "number" && Number.isFinite(ttft) && ttft >= 0 ? ttft : null, + itlMs: + typeof streamItlMs === "number" && Number.isFinite(streamItlMs) && streamItlMs >= 0 + ? streamItlMs + : null, + inputTokens: + streamUsage && typeof streamUsage === "object" + ? (() => { + const promptTokens = (streamUsage as Record).prompt_tokens; + return typeof promptTokens === "number" && Number.isFinite(promptTokens) + ? promptTokens + : null; + })() + : null, + outputTokens: + streamUsage && typeof streamUsage === "object" + ? (() => { + const completionTokens = (streamUsage as Record).completion_tokens; + return typeof completionTokens === "number" && Number.isFinite(completionTokens) + ? completionTokens + : null; + })() + : null, + cost: null, + retries: 0, + fallbackUsed: false, // combo-level fallback tracked by decisionTrace + outcome: + normalizedStreamStatus === 200 + ? "success" + : streamErrorCode === "stream_interrupted" || streamErrorCode === "aborted" + ? "stream_interrupted" + : outcomeFromStatus(normalizedStreamStatus), + status: normalizedStreamStatus, + finishReason: routingFinishReason(streamResponseBody), + connectionId: streamConnectionId ?? credentials?.connectionId ?? null, + }) + ); + persistAttemptLogs({ status: normalizedStreamStatus, error: streamError || undefined, diff --git a/open-sse/services/autoCombo/scoring.ts b/open-sse/services/autoCombo/scoring.ts index b8f66a797e..4c939501c7 100644 --- a/open-sse/services/autoCombo/scoring.ts +++ b/open-sse/services/autoCombo/scoring.ts @@ -23,6 +23,12 @@ export interface ScoringFactors { sessionAvailability?: number; resetWindowAffinity: number; connectionDensity: number; + /** + * Feedback-driven quality signal [0,1] from the routing-event quality tracker + * (open-sse/services/routing/quality.ts). Optional so cold candidates with no + * observed events default to neutral (1.0) and are never penalized. + */ + quality?: number; } export interface ScoringWeights { @@ -40,11 +46,13 @@ export interface ScoringWeights { sessionAvailability?: number; resetWindowAffinity: number; connectionDensity: number; + /** Weight for the feedback-driven quality factor (#feedback-foundation). */ + quality?: number; } export const DEFAULT_WEIGHTS: ScoringWeights = { quota: 0.1429, - health: 0.1905, + health: 0.1605, costInv: 0.1429, latencyInv: 0.1143, taskFit: 0.0762, @@ -57,6 +65,10 @@ export const DEFAULT_WEIGHTS: ScoringWeights = { sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + // Shifted from `health` (0.1905 → 0.1605): availability stays dominant, and + // the new quality signal (observed output quality over time) gets a real, + // if smaller, vote. Sum remains exactly 1.0. + quality: 0.03, }; /** Normalize independently configured UI weights into a scoring distribution. */ @@ -107,6 +119,12 @@ export interface ProviderCandidate { sessionAvailability?: number; /** Score [0..1] for quota reset-window preference; sooner selected reset windows score higher. */ resetWindowAffinity?: number; + /** + * Feedback-driven quality score [0..1] for this provider/model from the + * routing-event quality tracker (open-sse/services/routing). Omitted/undefined + * candidates default to a neutral 1.0 in calculateFactors. + */ + quality?: number; connectionPoolSize?: number; connectionId?: string; } @@ -141,7 +159,10 @@ export function calculateScore(factors: ScoringFactors, weights: ScoringWeights) (weights.cacheAffinity ?? 0) * (factors.cacheAffinity ?? 0) + (weights.sessionAvailability ?? 0) * (factors.sessionAvailability ?? 1) + (weights.resetWindowAffinity ?? 0) * factors.resetWindowAffinity + - (weights.connectionDensity ?? 0) * factors.connectionDensity + (weights.connectionDensity ?? 0) * factors.connectionDensity + + // Missing quality factor → neutral 0.5: a cold candidate is neither boosted + // (which would let optimistic initialization dominate) nor penalized. + (weights.quality ?? 0) * (factors.quality ?? 0.5) ); } @@ -268,6 +289,9 @@ export function calculateFactors( sessionAvailability: clamp01(candidate.sessionAvailability ?? 1), resetWindowAffinity: clamp01(candidate.resetWindowAffinity ?? 0.5), connectionDensity: clamp01(((candidate.connectionPoolSize ?? 1) - 1) / 10), + // Feedback quality signal; neutral 0.5 when the tracker has no data yet + // (cold providers are neither boosted nor unfairly penalized). + quality: clamp01(candidate.quality ?? 0.5), }; } diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index b6ae5d534a..cf602cf98d 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -36,6 +36,7 @@ import { import { buildNoUpstreamResponseDiagnostics, buildRecoveryHint } from "./combo/pinRecovery.ts"; import { buildTargetTimeoutRunner } from "./combo/targetTimeoutRunner.ts"; import { recordComboRequest, recordComboShadowRequest, getComboMetrics } from "./comboMetrics.ts"; +import { qualityScoreFor } from "./routing/index.ts"; import { expandComboSystemPromptIfPresent, resolveTargetFingerprint, @@ -578,6 +579,9 @@ export async function buildAutoCandidates( connectionPoolSize: connectionPoolCounts.get(provider) ?? 1, connectionId: target.connectionId ?? undefined, authType, + // Feedback-driven quality signal (routing quality tracker). Neutral 1.0 + // before enough samples accumulate — a cold model is never penalized. + quality: qualityScoreFor(provider, model), }; }) ); diff --git a/open-sse/services/combo/quotaShareStrategy.ts b/open-sse/services/combo/quotaShareStrategy.ts index e9e4293543..e12958042c 100644 --- a/open-sse/services/combo/quotaShareStrategy.ts +++ b/open-sse/services/combo/quotaShareStrategy.ts @@ -181,6 +181,7 @@ function applyDrr(targets: ResolvedComboTarget[], comboName: string): ResolvedCo const deficits = getDrrDeficits(comboName); const totalWeight = targets.reduce((sum, t) => sum + normalizeWeight(t.weight), 0); + if (totalWeight <= 0) return targets.slice(); // Add each target's quantum (weight share) to its deficit. for (const target of targets) { @@ -206,8 +207,9 @@ function applyDrr(targets: ResolvedComboTarget[], comboName: string): ResolvedCo return [winner, ...rest]; } -/** Weights default to 1 and are floored at 1 to keep quantum math well-defined. */ +/** Weights default to 1. Explicit 0 stays 0 so the operator can disable a target. */ function normalizeWeight(weight: number | undefined): number { + if (weight === 0) return 0; return Number.isFinite(weight) && (weight as number) > 0 ? (weight as number) : 1; } diff --git a/open-sse/services/routing/events.ts b/open-sse/services/routing/events.ts new file mode 100644 index 0000000000..955bbe28e2 --- /dev/null +++ b/open-sse/services/routing/events.ts @@ -0,0 +1,220 @@ +/** + * Routing Events — first-class representation of routing outcomes. + * + * Every request that reaches a provider emits one `RoutingEvent` describing what + * happened: which provider/model was used, under which strategy, with what + * latency/tokens/cost, and whether the outcome was a success, an error, a + * malformed response, a timeout, a rate-limit, or a blocked request. + * + * This is the "feedback foundation": the event is cheap to produce (no I/O in + * the emitting call) and is fanned out synchronously to registered sinks, each + * of which must be O(1)-ish and must never perform synchronous I/O. Sinks can + * then do whatever they need asynchronously — buffer to an OTLP exporter, + * update in-memory quality statistics, keep a bounded ring buffer for + * explainability, etc. + * + * DESIGN NOTE (adapted from the Future-AGI-inspired mission, kept deliberately + * lean): the original proposal was a Rust `RoutingEvent` struct + a + * `RoutingEventSink` trait. This module is the TypeScript equivalent, sized to + * the existing codebase: we already persist rich per-request detail in + * `call_logs` (async) and keep per-combo counters in `comboMetrics.ts`. This + * module adds the *typed, structured, sink-based* outcome channel those systems + * lacked, without duplicating either of them. + * + * SAFETY CONTRACT: an event carries ONLY routing metadata — provider, model, + * strategy, timing, token/cost numbers, an allowlisted outcome, finish reason, + * HTTP status, connection id. Never prompts, request/response bodies, headers, + * credentials, or account ids. + */ + +/** + * Allowlisted routing outcomes. Keeping this an enum-like union prevents freeform + * strings from leaking into telemetry/quality logic and keeps sinks exhaustive. + */ +export const ROUTING_OUTCOMES = [ + "success", + "error", + "malformed", + "timeout", + "rate_limited", + "stream_interrupted", + "guardrail_blocked", + "cancelled", +] as const; + +export type RoutingOutcome = (typeof ROUTING_OUTCOMES)[number]; + +export interface RoutingEvent { + /** Correlation/request id — never a prompt or body. */ + requestId: string; + provider: string; + model: string; + /** Combo strategy (e.g. "auto") or "direct" when not routed through a combo. */ + strategy: string; + latencyMs: number; + /** + * Time-to-first-forwarded-SSE-chunk in ms (NOT token-level TTFT), or null + * for non-streaming requests / when nothing was forwarded. + */ + ttftMs: number | null; + /** + * Mean inter-chunk gap in ms — a chunk-latency proxy for inter-token latency, + * only meaningful for streaming requests. Null otherwise. + */ + itlMs: number | null; + inputTokens: number | null; + outputTokens: number | null; + cost: number | null; + retries: number; + fallbackUsed: boolean; + outcome: RoutingOutcome; + /** Upstream HTTP status; null when the request never reached a provider. */ + status: number | null; + /** finish_reason from the provider response (stop / length / tool_calls / ...). */ + finishReason: string | null; + connectionId: string | null; + ts: number; +} + +/** A sink consumes routing events. Implementations must never do sync I/O. */ +export interface RoutingEventSink { + readonly name: string; + record(event: RoutingEvent): void; +} + +const sinks = new Set(); + +/** + * Register a sink. Returns an unsubscribe function. Registering the same sink + * instance twice is a no-op (Set semantics). + */ +export function registerRoutingEventSink(sink: RoutingEventSink): () => void { + sinks.add(sink); + return () => { + sinks.delete(sink); + }; +} + +/** Test/ops hook: list currently registered sink names. */ +export function listRoutingEventSinks(): string[] { + return Array.from(sinks, (s) => s.name); +} + +/** Test/ops hook: remove every registered sink. */ +export function clearRoutingEventSinks(): void { + sinks.clear(); +} + +/** + * Emit a routing event to every registered sink. Synchronous and allocation- + * friendly so callers can invoke it at the end of the request hot path without + * measurable impact; each sink's `record()` must be cheap (enqueue/buffer only). + * A throwing sink is isolated so one misbehaving sink cannot break the router. + */ +export function dispatchRoutingEvent(event: RoutingEvent): void { + for (const sink of sinks) { + try { + sink.record(event); + } catch { + // Sinks are observability/best-effort — never let one break the data plane. + } + } +} + +/** + * Bounded in-memory ring-buffer sink. Holds the most recent N events for + * explainability/debugging (see GET /api/v1/explain/routing). Insert is O(1); + * no TTL sweep needed because the buffer is size-bounded by construction. + */ +export class MemoryRoutingEventStore implements RoutingEventSink { + readonly name = "memory"; + private buffer: RoutingEvent[] = []; + private cursor = 0; + + constructor(private readonly capacity = 500) {} + + record(event: RoutingEvent): void { + if (this.buffer.length < this.capacity) { + this.buffer.push(event); + } else { + this.buffer[this.cursor] = event; + } + this.cursor = (this.cursor + 1) % this.capacity; + } + + /** Most recent events, newest first, up to `limit`. */ + recent(limit = 50): RoutingEvent[] { + if (this.buffer.length < this.capacity) { + return this.buffer.slice(-limit).reverse(); + } + // Ring is full — walk backwards from the cursor. + const out: RoutingEvent[] = []; + for (let i = 0; i < Math.min(limit, this.buffer.length); i++) { + const idx = (this.cursor - 1 - i + this.buffer.length) % this.buffer.length; + out.push(this.buffer[idx]); + } + return out; + } + + clear(): void { + this.buffer = []; + this.cursor = 0; + } + + get size(): number { + return this.buffer.length; + } +} + +/** Create a well-formed event with defaults for unset observability fields. */ +export function createRoutingEvent(input: { + requestId: string; + provider: string; + model: string; + strategy?: string | null; + latencyMs: number; + ttftMs?: number | null; + itlMs?: number | null; + inputTokens?: number | null; + outputTokens?: number | null; + cost?: number | null; + retries?: number; + fallbackUsed?: boolean; + outcome: RoutingOutcome; + status?: number | null; + finishReason?: string | null; + connectionId?: string | null; + ts?: number; +}): RoutingEvent { + return { + requestId: input.requestId, + provider: input.provider || "unknown", + model: input.model || "unknown", + strategy: input.strategy ?? "direct", + latencyMs: Math.max(0, input.latencyMs || 0), + ttftMs: input.ttftMs ?? null, + itlMs: input.itlMs ?? null, + inputTokens: input.inputTokens ?? null, + outputTokens: input.outputTokens ?? null, + cost: input.cost ?? null, + retries: input.retries ?? 0, + fallbackUsed: input.fallbackUsed ?? false, + outcome: input.outcome, + status: input.status ?? null, + finishReason: input.finishReason ?? null, + connectionId: input.connectionId ?? null, + ts: input.ts ?? Date.now(), + }; +} + +/** + * Classify an upstream HTTP status into a RoutingOutcome. Status 200/201 → success; + * 429 → rate_limited; 408/504 → timeout; 4xx/5xx → error; anything else → error. + */ +export function outcomeFromStatus(status: number | null | undefined): RoutingOutcome { + if (status == null) return "error"; + if (status === 200 || status === 201) return "success"; + if (status === 429) return "rate_limited"; + if (status === 408 || status === 504) return "timeout"; + return "error"; +} diff --git a/open-sse/services/routing/index.ts b/open-sse/services/routing/index.ts new file mode 100644 index 0000000000..1cb3077036 --- /dev/null +++ b/open-sse/services/routing/index.ts @@ -0,0 +1,133 @@ +/** + * Routing feedback foundation — default wiring. + * + * Bootstraps the default routing-event sinks: + * 1. `MemoryRoutingEventStore` — bounded ring buffer for explainability. + * 2. `QualityTracker` consumer — feeds the auto-combo `quality` scoring factor. + * 3. Optional OTel/HTTP exporter — enabled only when an OTLP endpoint is set. + * + * The hot path only calls `emitRoutingEvent()`, which fans out synchronously to + * these cheap in-memory sinks. No synchronous I/O, no external dependencies. + * + * This is the adapter seam Future AGI (or any evaluation backend) can plug into + * later without becoming a dependency: an evaluator would be another + * `RoutingEventSink` (or a consumer of the ring buffer / quality snapshot). + */ + +import { + clearRoutingEventSinks, + dispatchRoutingEvent, + listRoutingEventSinks, + MemoryRoutingEventStore, + registerRoutingEventSink, + type RoutingEvent, + type RoutingEventSink, +} from "./events.ts"; +import { + getProviderQuality, + getQualityScore, + getQualitySnapshot, + recordQualityEvent, + resetQualityTracker, + setSemanticQuality, + type ProviderQuality, +} from "./quality.ts"; +import { isRoutingOtelEnabled, OtlpHttpsEventSink } from "./otel.ts"; + +const memoryStore = new MemoryRoutingEventStore(500); + +// The quality tracker is registered as a sink so it updates inline with the +// event (O(1) math) and the OTel exporter only ever enqueues. +const qualitySink: RoutingEventSink = { + name: "quality", + record(event: RoutingEvent): void { + recordQualityEvent(event); + }, +}; + +let otelSink: OtlpHttpsEventSink | null = null; + +let initialized = false; + +/** Register the default sinks. Idempotent; safe to call multiple times. */ +export function initRoutingObservability(env: NodeJS.ProcessEnv = process.env): { + sinks: string[]; + otelEnabled: boolean; +} { + if (initialized) { + return { sinks: listRoutingSinkNames(), otelEnabled: isRoutingOtelEnabled(env) }; + } + initialized = true; + + registerRoutingEventSink(memoryStore); + registerRoutingEventSink(qualitySink); + + if (isRoutingOtelEnabled(env)) { + const endpoint = (env.OMNIROUTE_OTEL_ENDPOINT ?? env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "").trim(); + otelSink = new OtlpHttpsEventSink({ + endpoint, + serviceName: env.OTEL_SERVICE_NAME ?? "omniroute", + maxBatchSize: 64, + flushIntervalMs: 10_000, + }); + registerRoutingEventSink(otelSink); + } + + return { sinks: listRoutingSinkNames(), otelEnabled: otelSink != null }; +} + +/** Emit a routing event to all registered sinks (fire-and-forget, cheap). */ +export function emitRoutingEvent(event: RoutingEvent): void { + if (!initialized) initRoutingObservability(); + dispatchRoutingEvent(event); +} + +/** Neutral default quality used when a model has no observed events. */ +export function qualityScoreFor(provider: string, model: string): number { + return getQualityScore(provider, model); +} + +/** Full per-provider/model quality view (operational + semantic + confidence). */ +export function providerQualityFor(provider: string, model: string): ProviderQuality { + return getProviderQuality(provider, model); +} + +/** + * Evaluator seam: record a semantic quality score. NEVER call this from the + * request hot path with HTTP-derived signals — semantic quality is reserved for + * actual evaluation (task success, tool-use correctness, groundedness). + */ +export { setSemanticQuality } from "./quality.ts"; + +export function routingQualitySnapshot(limit = 200): ReturnType { + return getQualitySnapshot(limit); +} + +export { classifyQuality, type QualityClassification } from "./quality.ts"; + +export function recentRoutingEvents(limit = 50): RoutingEvent[] { + return memoryStore.recent(limit); +} + +export function routingOtelStats(): { buffered: number; dropped: number } | null { + return otelSink ? otelSink.getStats() : null; +} + +function listRoutingSinkNames(): string[] { + return listRoutingEventSinks(); +} + +/** Test/ops hook: full reset of the routing observability layer. */ +export function resetRoutingObservability(): void { + clearRoutingEventSinks(); + memoryStore.clear(); + resetQualityTracker(); + if (otelSink) { + otelSink.stop(); + otelSink = null; + } + initialized = false; +} + +export type { RoutingEvent, RoutingOutcome, RoutingEventSink } from "./events.ts"; +export { createRoutingEvent, outcomeFromStatus } from "./events.ts"; diff --git a/open-sse/services/routing/otel.ts b/open-sse/services/routing/otel.ts new file mode 100644 index 0000000000..23578011c0 --- /dev/null +++ b/open-sse/services/routing/otel.ts @@ -0,0 +1,227 @@ +/** + * Optional OpenTelemetry / GenAI observability sink. + * + * A `RoutingEventSink` that forwards routing events to an OTLP/HTTP collector as + * GenAI semantic-convention spans (semconvgenai: `gen_ai.provider.name`, + * `gen_ai.request.model`, `gen_ai.operation.name`, `gen_ai.usage.input_tokens`, + * `gen_ai.usage.output_tokens`, etc.). + * + * Deliberately lightweight: + * - No `@opentelemetry/*` SDK dependency. Uses the collector's OTLP/HTTP JSON + * (traces) endpoint via global `fetch`, which is already available and async. + * - `record()` only enqueues into a bounded buffer (O(1), never I/O). A single + * background flush timer drains the buffer asynchronously. Under overload the + * oldest events are dropped (never backpressure the data plane). + * - Disabled unless `OMNIROUTE_OTEL_ENDPOINT` (or `OTEL_EXPORTER_OTLP_ENDPOINT`) + * is set — normal lightweight deployments run with zero OTel code executing. + * - No secrets/prompts are ever serialized; only RoutingEvent metadata. + */ + +export interface OtlpHttpsExporterConfig { + /** Collector base URL, e.g. https://collector:4318 — spans go to /v1/traces. */ + endpoint: string; + /** Export batch size / flush interval. */ + maxBatchSize?: number; + flushIntervalMs?: number; + serviceName?: string; +} + +/** Resolve whether OTLP export is configured. */ +export function isRoutingOtelEnabled(env: NodeJS.ProcessEnv = process.env): boolean { + const endpoint = (env.OMNIROUTE_OTEL_ENDPOINT ?? env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "").trim(); + return endpoint.length > 0; +} + +interface OtelSpan { + traceId: string; + spanId: string; + name: string; + kind: number; + startTimeUnixNano: string; + endTimeUnixNano: string; + attributes: Array<{ + key: string; + value: { stringValue?: string; intValue?: string; doubleValue?: number }; + }>; +} + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +} + +function randomId(bytes: number): string { + const arr = new Uint8Array(bytes); + // Use crypto.getRandomValues when available (Node ≥ 19 global), else Math.random. + if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") { + crypto.getRandomValues(arr); + } else { + for (let i = 0; i < bytes; i++) arr[i] = Math.floor(Math.random() * 256); + } + return toHex(arr); +} + +export class OtlpHttpsEventSink { + readonly name = "otel"; + private readonly endpoint: string; + private readonly maxBatchSize: number; + private readonly serviceName: string; + private buffer: RoutingEventLike[] = []; + private dropped = 0; + private consecutiveFailures = 0; + private flushedBatches = 0; + private timer: ReturnType | null = null; + private flushing = false; + + constructor(private readonly config: OtlpHttpsExporterConfig) { + this.endpoint = config.endpoint.replace(/\/+$/, "") + "/v1/traces"; + this.maxBatchSize = config.maxBatchSize ?? 64; + this.serviceName = config.serviceName ?? "omniroute"; + this.start(); + } + + /** O(1) enqueue; drops oldest when the buffer is full. Never performs I/O. */ + record(event: RoutingEventLike): void { + if (this.buffer.length >= this.maxBatchSize * 4) { + this.buffer.shift(); + this.dropped += 1; + } + this.buffer.push(event); + } + + getStats(): { + buffered: number; + dropped: number; + consecutiveFailures: number; + flushedBatches: number; + } { + return { + buffered: this.buffer.length, + dropped: this.dropped, + consecutiveFailures: this.consecutiveFailures, + flushedBatches: this.flushedBatches, + }; + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + void this.flush(); + } + + private start(): void { + const intervalMs = this.config.flushIntervalMs ?? 10_000; + this.timer = setInterval(() => void this.flush(), intervalMs); + // Do not keep the process alive just for telemetry. + this.timer.unref?.(); + } + + private async flush(): Promise { + if (this.flushing) return; + if (this.buffer.length === 0) return; + this.flushing = true; + const batch = this.buffer.splice(0, this.maxBatchSize); + try { + const res = await fetch(this.endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(buildOtlpTracesPayload(batch, this.serviceName)), + signal: AbortSignal.timeout(3000), + }); + if (!res.ok) throw new Error(`OTLP collector returned ${res.status}`); + this.consecutiveFailures = 0; + this.flushedBatches += 1; + } catch { + // Telemetry delivery is best-effort. Re-buffer for a retry, but stop after + // MAX_CONSECUTIVE_FAILURES so a permanently-unavailable collector cannot + // grow the buffer without bound. The dropped counter reflects the loss. + this.consecutiveFailures += 1; + if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + this.dropped += batch.length; + } else { + this.buffer.unshift(...batch); + } + } finally { + this.flushing = false; + } + } +} + +/** Drop a batch (and count it) after this many consecutive collector failures. */ +const MAX_CONSECUTIVE_FAILURES = 5; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RoutingEventLike = any; + +/** + * Build an OTLP/HTTP traces JSON payload with one span per routing event, + * mapped to GenAI semantic conventions. + */ +export function buildOtlpTracesPayload(events: RoutingEventLike[], serviceName: string): unknown { + const resourceSpans = [ + { + resource: { + attributes: [ + { key: "service.name", value: { stringValue: serviceName } }, + { key: "telemetry.sdk.name", value: { stringValue: "omniroute-routing" } }, + ], + }, + scopeSpans: [ + { + scope: { name: "omniroute.routing" }, + spans: events.map(toSpan), + }, + ], + }, + ]; + return { resourceSpans }; +} + +function attr( + key: string, + value: string | number +): { key: string; value: { stringValue?: string; intValue?: string; doubleValue?: number } } { + if (typeof value === "number") { + return Number.isInteger(value) + ? { key, value: { intValue: String(value) } } + : { key, value: { doubleValue: value } }; + } + return { key, value: { stringValue: String(value) } }; +} + +function toSpan(event: RoutingEventLike): OtelSpan { + const traceId = randomId(16); + const spanId = randomId(8); + const startNs = BigInt(event.ts) * 1_000_000n; + const endNs = startNs + BigInt(Math.max(0, event.latencyMs || 0)) * 1_000_000n; + const attributes = [ + attr("gen_ai.provider.name", event.provider), + attr("gen_ai.request.model", event.model), + attr("gen_ai.operation.name", "chat"), + attr("gen_ai.system", event.strategy || "direct"), + attr("gen_ai.usage.input_tokens", event.inputTokens ?? 0), + attr("gen_ai.usage.output_tokens", event.outputTokens ?? 0), + attr("gen_ai.completion.finish_reason", event.finishReason ?? "unknown"), + attr("gen_ai.request.temperature", 0), + attr("omniroute.routing.outcome", event.outcome), + attr("omniroute.routing.status", event.status ?? 0), + attr("omniroute.routing.ttft_ms", event.ttftMs ?? -1), + attr("omniroute.routing.itl_ms", event.itlMs ?? -1), + attr("omniroute.routing.retries", event.retries ?? 0), + attr("omniroute.routing.fallback_used", event.fallbackUsed ? 1 : 0), + attr("gen_ai.client.token.usage.input_tokens", event.inputTokens ?? 0), + attr("gen_ai.client.token.usage.output_tokens", event.outputTokens ?? 0), + ]; + if (event.connectionId) attributes.push(attr("omniroute.connection_id", event.connectionId)); + + return { + traceId, + spanId, + name: `chat ${event.provider}/${event.model}`, + kind: 3, // CLIENT + startTimeUnixNano: startNs.toString(), + endTimeUnixNano: endNs.toString(), + attributes, + }; +} diff --git a/open-sse/services/routing/quality.ts b/open-sse/services/routing/quality.ts new file mode 100644 index 0000000000..b4e06a9ed9 --- /dev/null +++ b/open-sse/services/routing/quality.ts @@ -0,0 +1,313 @@ +/** + * Provider/Model Quality Signal — feedback-driven adaptive routing (v2). + * + * v2 separates two distinct concepts that v1 conflated: + * + * - **Operational quality** — derived from the routing hot path (HTTP status, + * connection failures, 429s, malformed responses, stream interruptions, + * finish_reason anomalies, zero-output successes, latency/TTFT). A request + * returning HTTP 200 is NOT necessarily high quality; operational quality + * only says "the wire behaved." + * - **Semantic quality** — the actual value of the generated output + * (evaluator score, task success, tool-use correctness, factual accuracy). + * This is ONLY ever produced by an external evaluator via + * `setSemanticQuality()`. It is never manufactured from HTTP success. It is + * `null` until an evaluator provides a value. + * + * Confidence / sample awareness (v2): + * - `confidence = clamp01(samples / CONFIDENCE_FULL_SAMPLES)`. + * - The score returned to the scorer is blended toward the neutral midpoint + * (0.5): `score = NEUTRAL + confidence * (operational - NEUTRAL)`. + * - Consequences: a cold provider (0 samples) scores neutral 0.5 — it is not + * unfairly penalized, but it also cannot dominate a provider with thousands + * of solid observations. A provider with 7 lucky successes is pulled toward + * 0.5, so it never dominates purely from optimistic initialization. + * + * This complements the existing resilience stack (circuit breaker, connection + * cooldown, model lockout, health matrix): those handle *availability* (hard + * exclusion); this signal handles *soft adaptive preference*. + * + * Statistics are plain arithmetic (EWMA + small counters), O(1) per event, safe + * under the Node event loop's single thread — no lock-free/atomic trickery. + */ + +/** EWMA smoothing factor (alpha). Lower = slower adaptation. */ +const OPERATIONAL_ALPHA = 0.2; +/** Latency EWMA alpha — slower so transient spikes don't tank quality instantly. */ +const LATENCY_ALPHA = 0.1; +/** Samples at which confidence reaches 1.0 (full confidence). */ +const CONFIDENCE_FULL_SAMPLES = 50; +/** Neutral score used for cold/unknown providers (midpoint, neither boosted nor penalized). */ +const NEUTRAL_SCORE = 0.5; + +interface QualityState { + /** EWMA of the success indicator (1 = good, 0 = bad). */ + successEwma: number; + /** EWMA of latency in ms. */ + latencyEwma: number; + /** EWMA of TTFT in ms (streaming only). */ + ttftEwma: number | null; + /** Total events observed for this (provider, model). */ + samples: number; + /** Count of operational-anomaly events (malformed / empty / length / interrupted). */ + anomalies: number; + /** Rate-limit (429) count — tracked separately for observability. */ + rateLimited: number; + /** Semantic quality [0,1] from an external evaluator, if one has provided it. */ + semantic: number | null; + /** Confidence [0,1] of the semantic score as reported by the evaluator. */ + semanticConfidence: number | null; + lastTs: number; +} + +const states = new Map(); + +function keyOf(provider: string, model: string): string { + return `${provider}/${model}`; +} + +function getOrCreate(key: string): QualityState { + let state = states.get(key); + if (!state) { + state = { + successEwma: 1, + latencyEwma: 0, + ttftEwma: null, + samples: 0, + anomalies: 0, + rateLimited: 0, + semantic: null, + semanticConfidence: null, + lastTs: 0, + }; + states.set(key, state); + } + return state; +} + +function isOperationalAnomaly(event: { + outcome: string; + finishReason: string | null; + outputTokens: number | null | undefined; +}): boolean { + if (event.outcome === "malformed" || event.outcome === "stream_interrupted") return true; + // finish_reason=length → the model ran out of output budget (truncated answer). + if (event.outcome === "success" && event.finishReason === "length") return true; + // A "successful" 200 that produced zero output tokens is an empty/invalid output. + // NOTE: we deliberately do NOT treat a missing finish_reason as an anomaly — + // streaming passthrough frequently has no reconstructed finish_reason, so that + // signal would penalize every legitimately streamed request (pure noise). + if (event.outcome === "success" && event.outputTokens === 0) return true; + return false; +} + +function successIndicator(event: { outcome: string; status: number | null }): number { + if (event.outcome === "success") return 1; + // 429 is a transient signal, not a quality failure — treat as neutral-positive. + if (event.outcome === "rate_limited" || event.status === 429) return 0.5; + return 0; +} + +/** Record one operational routing event into the quality estimate. O(1). */ +export function recordQualityEvent(event: { + provider: string; + model: string; + outcome: string; + status: number | null; + latencyMs: number; + ttftMs?: number | null; + finishReason?: string | null; + outputTokens?: number | null; + ts?: number; +}): void { + const key = keyOf(event.provider || "unknown", event.model || "unknown"); + const state = getOrCreate(key); + + state.samples += 1; + if ( + isOperationalAnomaly({ + outcome: event.outcome, + finishReason: event.finishReason ?? null, + outputTokens: event.outputTokens ?? undefined, + }) + ) { + state.anomalies += 1; + } + if (event.outcome === "rate_limited" || event.status === 429) state.rateLimited += 1; + + const indicator = successIndicator({ outcome: event.outcome, status: event.status }); + // First sample seeds the EWMA directly (no lag toward a default). + state.successEwma = + state.samples === 1 + ? indicator + : state.successEwma + OPERATIONAL_ALPHA * (indicator - state.successEwma); + + const latency = Number.isFinite(event.latencyMs) && event.latencyMs >= 0 ? event.latencyMs : 0; + state.latencyEwma = + state.samples === 1 + ? latency + : state.latencyEwma + LATENCY_ALPHA * (latency - state.latencyEwma); + + const ttft = event.ttftMs; + if (typeof ttft === "number" && Number.isFinite(ttft) && ttft >= 0) { + state.ttftEwma = + state.ttftEwma == null ? ttft : state.ttftEwma + LATENCY_ALPHA * (ttft - state.ttftEwma); + } + + state.lastTs = event.ts ?? Date.now(); +} + +/** + * Evaluator seam: record a semantic quality score for a (provider, model). + * Semantic quality is ONLY ever produced by an evaluator (deterministic scorer, + * local LLM judge, HTTP/Future-AGI adapter, WASM). It is never manufactured from + * operational/HTP success. `confidence` should reflect the evaluator's certainty + * (e.g. number of eval cases backing the score). + */ +export function setSemanticQuality( + provider: string, + model: string, + score: number, + confidence: number +): void { + const state = getOrCreate(keyOf(provider || "unknown", model || "unknown")); + state.semantic = Math.max(0, Math.min(1, Number.isFinite(score) ? score : 0.5)); + state.semanticConfidence = Math.max(0, Math.min(1, Number.isFinite(confidence) ? confidence : 0)); +} + +export interface ProviderQuality { + provider: string; + model: string; + /** Operational score [0,1] (wire behavior) — confidence-adjusted, neutral 0.5 cold. */ + operational: number; + /** Semantic score [0,1] from an evaluator, or null when none has been provided. */ + semantic: number | null; + /** Confidence [0,1] of the operational score (sample-count based). */ + confidence: number; + /** Confidence [0,1] of the semantic score, when an evaluator reported one. */ + semanticConfidence: number | null; + samples: number; + anomalies: number; + rateLimited: number; + successEwma: number; + latencyEwmaMs: number; + ttftEwmaMs: number | null; + /** Milliseconds since the last observed event; null when never observed. */ + recencyMs: number | null; + lastTs: number; +} + +/** Raw operational score before the confidence blend (pure EWMA + penalties). */ +function rawOperationalScore(state: QualityState): number { + let score = state.successEwma; + + // Latency degradation: soft penalty capped at 0.2 so slow models are discounted, not zeroed. + const latencyPenalty = Math.min(0.2, state.latencyEwma / 60_000); + score -= latencyPenalty; + + // Anomaly penalty: capped so a few bad apples don't nuke a provider entirely. + const anomalyRate = state.anomalies / Math.max(1, state.samples); + score -= Math.min(0.25, anomalyRate * 0.5); + + return Math.max(0, Math.min(1, score)); +} + +function confidenceOf(samples: number): number { + return Math.max(0, Math.min(1, samples / CONFIDENCE_FULL_SAMPLES)); +} + +/** + * Operational quality for a (provider, model), confidence-adjusted and blended + * toward the neutral midpoint. See module docs for the cold-start guarantee. + */ +export function getProviderQuality(provider: string, model: string): ProviderQuality { + const state = states.get(keyOf(provider, model)); + const now = Date.now(); + if (!state || state.samples === 0) { + return { + provider, + model, + operational: NEUTRAL_SCORE, + semantic: null, + confidence: 0, + semanticConfidence: null, + samples: 0, + anomalies: 0, + rateLimited: 0, + successEwma: 1, + latencyEwmaMs: 0, + ttftEwmaMs: null, + recencyMs: null, + lastTs: 0, + }; + } + const confidence = confidenceOf(state.samples); + const raw = rawOperationalScore(state); + const operational = NEUTRAL_SCORE + confidence * (raw - NEUTRAL_SCORE); + return { + provider, + model, + operational, + semantic: state.semantic, + confidence, + semanticConfidence: state.semanticConfidence, + samples: state.samples, + anomalies: state.anomalies, + rateLimited: state.rateLimited, + successEwma: state.successEwma, + latencyEwmaMs: state.latencyEwma, + ttftEwmaMs: state.ttftEwma, + recencyMs: state.samples > 0 ? Math.max(0, now - state.lastTs) : null, + lastTs: state.lastTs, + }; +} + +/** + * Backward-compatible scalar used by the auto-combo scorer's `quality` factor. + * Returns the confidence-adjusted operational score (neutral 0.5 when cold). + */ +export function getQualityScore(provider: string, model: string): number { + return getProviderQuality(provider, model).operational; +} + +/** Full snapshot of the tracker for explainability / dashboard. */ +export function getQualitySnapshot(limit = 200): ProviderQuality[] { + const views: ProviderQuality[] = []; + for (const [key] of states) { + const slash = key.indexOf("/"); + const provider = slash >= 0 ? key.slice(0, slash) : key; + const model = slash >= 0 ? key.slice(slash + 1) : key; + views.push(getProviderQuality(provider, model)); + } + views.sort((a, b) => b.lastTs - a.lastTs); + return views.slice(0, limit); +} + +/** + * Classify a provider/model quality state for explainability / dashboard. + * This reflects the SOFT adaptive signal — it says nothing about hard exclusion + * (circuit open / quota / auth), which is owned by the resilience stack. + * + * - "healthy": high confidence + operational quality well above neutral + * - "degraded": operational quality at or below neutral (soft penalty active) + * - "warming": low confidence (few samples) — treated neutrally + * - "cold": never observed — neutral, cannot dominate + */ +export type QualityClassification = "healthy" | "degraded" | "warming" | "cold"; + +export function classifyQuality(q: ProviderQuality): QualityClassification { + if (q.samples === 0) return "cold"; + if (q.confidence < 0.5) return "warming"; + if (q.operational < 0.5) return "degraded"; + return "healthy"; +} + +/** Test/ops hook: reset all quality state. */ +export function resetQualityTracker(): void { + states.clear(); +} + +export const QUALITY_WELL_KNOWN = { + CONFIDENCE_FULL_SAMPLES, + NEUTRAL_SCORE, +} as const; diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index e6547bb3e5..275669e983 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -81,6 +81,7 @@ import { import { restoreClaudeToolName } from "../services/claudeCodeToolRemapper.ts"; import { normalizeFinalOpenAIStreamChunk } from "./openAIStreamChunk.ts"; import { collectClaudeDelta } from "./streamClaudeDelta.ts"; +import { createStreamTiming, type StreamTiming } from "./streamTiming.ts"; /** * Race a response body read against a timeout. @@ -129,7 +130,15 @@ type StreamCompletePayload = { clientPayload?: unknown; error?: string | null; errorCode?: string | null; + /** + * Time-to-first-forwarded-SSE-chunk in ms, or null when nothing was forwarded. + * NOT token-level TTFT — see open-sse/utils/streamTiming.ts for what is measured. + */ ttft?: number | null; + /** Mean inter-chunk gap in ms (chunk-latency proxy for ITL), or null. */ + itlMs?: number | null; + /** True when the stream was interrupted (timeout/abort/error) before a clean finish. */ + interrupted?: boolean; }; type StreamOptions = { @@ -577,7 +586,10 @@ function getOpenAIIntermediateChunks(value: unknown): unknown[] { return Array.isArray(candidate) ? candidate : []; } -export function restoreClaudePassthroughToolUseName(parsed: JsonRecord, toolNameMap: unknown): boolean { +export function restoreClaudePassthroughToolUseName( + parsed: JsonRecord, + toolNameMap: unknown +): boolean { const block = parsed.content_block && typeof parsed.content_block === "object" ? (parsed.content_block as JsonRecord) @@ -660,6 +672,16 @@ export function createSSEStream(options: StreamOptions = {}) { performance.clearMarks("omni-request-body-size"); } + // Canonical streaming timing (TTFT / ITL / interruption). One instance per + // stream, marked from the transform below. ttft() = first-forwarded-SSE-chunk + // latency (NOT token-level) — see streamTiming.ts. + const timing: StreamTiming = createStreamTiming(); + /** Forward a pre-encoded SSE chunk, marking TTFT/ITL on the way. */ + const forward = (controller: TransformStreamDefaultController, bytes: Uint8Array) => { + timing.markForward(); + controller.enqueue(bytes); + }; + // Drop internal commentary-phase Responses output before forwarding (#6199). // Explicit option wins; otherwise read the feature flag (default on) — resolved once per stream. const shouldDropResponsesCommentary = @@ -948,7 +970,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayloadCollector.push(event); const output = formatSSE(event, FORMATS.CLAUDE); reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); } }; @@ -973,7 +995,8 @@ export function createSSEStream(options: StreamOptions = {}) { const errOutput = formatSSE(errorEvent, FORMATS.CLAUDE); reqLogger?.appendConvertedChunk?.(errOutput); clientPayloadCollector.push(errorEvent); - controller.enqueue(encoder.encode(errOutput)); + forward(controller, encoder.encode(errOutput)); + timing.markInterrupted(); let failureHandled = false; if (onFailure) { try { @@ -1034,7 +1057,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayloadCollector.push(itemSanitized); reqLogger?.appendConvertedChunk?.(output); forwardedValuableChunk = true; - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); }; const emitFinalSseMetadata = async ( @@ -1059,7 +1082,7 @@ export function createSSEStream(options: StreamOptions = {}) { }); if (!comment) return; reqLogger?.appendConvertedChunk?.(comment); - controller.enqueue(encoder.encode(comment)); + forward(controller, encoder.encode(comment)); }; const getResponsesReasoningKey = (payload: Record): string | null => { @@ -1146,7 +1169,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayloadCollector.push(syntheticEvent.body); const output = `event: ${syntheticEvent.event}\ndata: ${JSON.stringify(syntheticEvent.body)}\n\n`; reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); } }; @@ -1164,6 +1187,7 @@ export function createSSEStream(options: StreamOptions = {}) { let failureHandled = false; if (onFailure) { try { + timing.markInterrupted(); failureHandled = onFailure({ status: HTTP_STATUS.GATEWAY_TIMEOUT, @@ -1195,6 +1219,7 @@ export function createSSEStream(options: StreamOptions = {}) { transform(chunk, controller) { if (streamTimedOut) return; const now = Date.now(); + timing.markByte(); lastChunkTime = now; const text = decoder.decode(chunk, { stream: true }); buffer += text; @@ -1253,7 +1278,7 @@ export function createSSEStream(options: StreamOptions = {}) { const pendingOutput = passthroughEventPrefix.flush(); if (pendingOutput) { reqLogger?.appendConvertedChunk?.(pendingOutput); - controller.enqueue(encoder.encode(pendingOutput)); + forward(controller, encoder.encode(pendingOutput)); } clearPendingPassthroughEvent(); continue; @@ -1420,7 +1445,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayloadCollector.push(event); } reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); injectedUsage = true; } else { output = `data: ${JSON.stringify(parsed)}\n\n`; @@ -1709,7 +1734,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayload = parsed; clientPayloadCollector.push(clientPayload); reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); continue; } @@ -1785,7 +1810,7 @@ export function createSSEStream(options: StreamOptions = {}) { totalContentLength += delta.reasoning_content.length; clientPayloadCollector.push(reasoningChunk); reqLogger?.appendConvertedChunk?.(rOutput); - controller.enqueue(encoder.encode(rOutput)); + forward(controller, encoder.encode(rOutput)); delete delta.reasoning_content; splitMixedReasoningContent = true; } @@ -1964,7 +1989,7 @@ export function createSSEStream(options: StreamOptions = {}) { } reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); if (failurePayload) { let failureHandled = false; if (onFailure) { @@ -2004,7 +2029,7 @@ export function createSSEStream(options: StreamOptions = {}) { if (parsed.error) { const output = formatTranslatedStreamError(parsed, sourceFormat); reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); upstreamErrorForwarded = true; doneSent = true; continue; @@ -2223,7 +2248,7 @@ export function createSSEStream(options: StreamOptions = {}) { passthroughEventPrefix, emitConvertedOutput: (output: string) => { reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); }, pushProviderPayload: (payload: unknown) => providerPayloadCollector.push(payload), pushClientPayload: (payload: unknown) => clientPayloadCollector.push(payload), @@ -2336,7 +2361,7 @@ export function createSSEStream(options: StreamOptions = {}) { output = output.endsWith("\n") ? `${output}\n` : `${output}\n\n`; } reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + forward(controller, encoder.encode(output)); } if (shouldInjectClaudeEmptyResponseOnFlush(claudeEmptyResponseLifecycle)) { @@ -2380,7 +2405,7 @@ export function createSSEStream(options: StreamOptions = {}) { flushOutput = `data: ${JSON.stringify(syntheticChunk)}\n\n`; } reqLogger?.appendConvertedChunk?.(flushOutput); - controller.enqueue(encoder.encode(flushOutput)); + forward(controller, encoder.encode(flushOutput)); passthroughAccumulatedContent = appendBoundedText( passthroughAccumulatedContent, passthroughBufferedTextualToolCallContent @@ -2397,7 +2422,7 @@ export function createSSEStream(options: StreamOptions = {}) { totalContentLength += thinkFlush.addedLength; clientPayloadCollector.push(thinkFlush.syntheticChunk); reqLogger?.appendConvertedChunk?.(thinkFlush.flushOutput); - controller.enqueue(encoder.encode(thinkFlush.flushOutput)); + forward(controller, encoder.encode(thinkFlush.flushOutput)); } // Estimate usage if provider didn't return valid usage @@ -2431,7 +2456,7 @@ export function createSSEStream(options: StreamOptions = {}) { ); const finishOutput = `data: ${JSON.stringify(syntheticFinishChunk)}\n\n`; reqLogger?.appendConvertedChunk?.(finishOutput); - controller.enqueue(encoder.encode(finishOutput)); + forward(controller, encoder.encode(finishOutput)); clientPayloadCollector.push(syntheticFinishChunk); } await emitFinalSseMetadata(controller, usage); @@ -2440,7 +2465,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayloadCollector.push({ done: true }); const doneOutput = "data: [DONE]\n\n"; reqLogger?.appendConvertedChunk?.(doneOutput); - controller.enqueue(encoder.encode(doneOutput)); + forward(controller, encoder.encode(doneOutput)); } } // Notify caller for call log persistence (include full response body with accumulated content) @@ -2514,6 +2539,9 @@ export function createSSEStream(options: StreamOptions = {}) { status: 200, usage, responseBody, + ttft: timing.ttftMs(), + itlMs: timing.avgItlMs(), + interrupted: timing.interrupted, // #9315 switched the summary to the accumulated responseBody to avoid // stale/truncated event data — but responseBody here is synthesized in // chat-completion shape, which loses the Responses API `response` object. @@ -2616,6 +2644,7 @@ export function createSSEStream(options: StreamOptions = {}) { let failureHandled = false; if (onFailure) { try { + timing.markInterrupted(); failureHandled = onFailure({ status: err.status, @@ -2635,6 +2664,9 @@ export function createSSEStream(options: StreamOptions = {}) { status: err.status, usage: state?.usage, responseBody: errorBody, + ttft: timing.ttftMs(), + itlMs: timing.avgItlMs(), + interrupted: timing.interrupted, error: err.message, errorCode: err.code, providerPayload: providerPayloadCollector.build( @@ -2731,7 +2763,7 @@ export function createSSEStream(options: StreamOptions = {}) { clientPayloadCollector.push({ done: true }); const doneOutput = "data: [DONE]\n\n"; reqLogger?.appendConvertedChunk?.(doneOutput); - controller.enqueue(encoder.encode(doneOutput)); + forward(controller, encoder.encode(doneOutput)); } } diff --git a/open-sse/utils/streamTiming.ts b/open-sse/utils/streamTiming.ts new file mode 100644 index 0000000000..9f26f5d5b3 --- /dev/null +++ b/open-sse/utils/streamTiming.ts @@ -0,0 +1,83 @@ +/** + * Canonical streaming timing instrumentation (TTFT / ITL / interruption). + * + * One reusable seam for measuring the streaming path. It is created once per + * stream and marked from the SSE transform: + * + * markByte() — first upstream chunk received (bytes arrived from provider) + * markForward() — first chunk forwarded to the client (first SSE chunk enqueued) + * + * `ttft()` is therefore **first-forwarded-SSE-chunk latency**, NOT token-level + * TTFT. We document that distinction explicitly: a single SSE chunk can carry + * zero, one, or many tokens, and chunk boundaries do not map to token + * boundaries. If a future implementation can measure actual token timing it + * should extend this seam, not bypass it. + * + * ITL (inter-token latency) is approximated by the mean gap between forwarded + * SSE chunks (bounded sample window). It is a chunk-latency proxy, again not + * true token timing — callers must label it as such. + * + * The object is cheap to construct, plain mutable state, and safe under the + * event loop's single thread (each stream owns its own instance). + */ +export interface StreamTiming { + startedAt: number; + firstByteAt: number | null; + firstForwardAt: number | null; + lastForwardAt: number | null; + /** Mean gap between forwarded chunks (ms), bounded window. */ + interChunkGaps: number[]; + forwardedChunks: number; + interrupted: boolean; + markByte(): void; + markForward(): void; + markInterrupted(): void; + /** First-forwarded-SSE-chunk latency in ms, or null if nothing was forwarded. */ + ttftMs(): number | null; + /** Mean inter-chunk gap in ms, or null when fewer than 2 chunks were forwarded. */ + avgItlMs(): number | null; + /** Time from stream start to completion (ms). */ + totalMs(): number; +} + +/** Max number of inter-chunk samples kept (bounds memory). */ +const MAX_INTER_CHUNK_GAPS = 32; + +export function createStreamTiming(): StreamTiming { + const timing: StreamTiming = { + startedAt: Date.now(), + firstByteAt: null, + firstForwardAt: null, + lastForwardAt: null, + interChunkGaps: [], + forwardedChunks: 0, + interrupted: false, + markByte() { + if (this.firstByteAt === null) this.firstByteAt = Date.now(); + }, + markForward() { + const now = Date.now(); + if (this.firstForwardAt === null) this.firstForwardAt = now; + if (this.lastForwardAt !== null && this.interChunkGaps.length < MAX_INTER_CHUNK_GAPS) { + this.interChunkGaps.push(now - this.lastForwardAt); + } + this.lastForwardAt = now; + this.forwardedChunks += 1; + }, + markInterrupted() { + this.interrupted = true; + }, + ttftMs() { + return this.firstForwardAt === null ? null : this.firstForwardAt - this.startedAt; + }, + avgItlMs() { + if (this.interChunkGaps.length === 0) return null; + const sum = this.interChunkGaps.reduce((a, b) => a + b, 0); + return sum / this.interChunkGaps.length; + }, + totalMs() { + return Date.now() - this.startedAt; + }, + }; + return timing; +} diff --git a/package.json b/package.json index a199504707..fdacc42ef4 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "gen:provider-reference": "bun scripts/docs/gen-provider-reference.ts", "bench:compression": "bun scripts/compression/benchmark.ts", "bench:heap-body": "node --expose-gc --import tsx/esm scripts/perf/request-body-heap.ts", + "bench:routing-events": "node --import tsx/esm scripts/perf/routing-events-bench.ts", "eval:compression": "node --import tsx scripts/compression-eval/index.ts", "eval:router": "node --import tsx scripts/router-eval/index.ts", "eval:router:compare": "node --import tsx scripts/router-eval/compare.ts", diff --git a/scripts/perf/routing-events-bench.ts b/scripts/perf/routing-events-bench.ts new file mode 100644 index 0000000000..bf89c4a3fc --- /dev/null +++ b/scripts/perf/routing-events-bench.ts @@ -0,0 +1,175 @@ +/** + * Routing feedback foundation benchmark (v2 — honest comparison). + * + * v1 reported a single "~0.2µs/request" figure. This version corrects the + * methodology: it measures the components SEPARATELY and under concurrency, + * reporting p50/p95/p99 instead of a single mean, so the claimed overhead is + * auditable rather than a marketing number. + * + * Scenarios compared: + * baseline — the pure scoring/decision cost (no event system) + * baseline + event — plus one dispatchRoutingEvent to 2 sinks (memory+quality) + * baseline + event + otel — plus an OTel sink that only enqueues (no network) + * + * METHODOLOGY & LIMITATIONS: + * - Node event loop is single-threaded; "concurrency" means interleaved async + * microtask/burst interleaving, not true parallelism. + * - p95/p99 are measured per-op over a big N with high-resolution timers. + * - No network I/O is performed (OTel flush is deliberately not fired). + * - Numbers are machine-specific; treat them as relative, not absolute. + * + * Usage: + * npm run bench:routing-events + * npm run bench:routing-events -- --events 200000 + */ +import { performance } from "node:perf_hooks"; + +import { + dispatchRoutingEvent, + MemoryRoutingEventStore, + registerRoutingEventSink, + type RoutingEvent, + type RoutingEventSink, +} from "../../open-sse/services/routing/events.ts"; +import { recordQualityEvent } from "../../open-sse/services/routing/quality.ts"; +import { OtlpHttpsEventSink } from "../../open-sse/services/routing/otel.ts"; +import { + calculateFactors, + calculateScore, + DEFAULT_WEIGHTS, + type ProviderCandidate, +} from "../../open-sse/services/autoCombo/scoring.ts"; + +const N = Number(process.argv[2] === "--events" ? (process.argv[3] ?? 100_000) : 100_000); + +function makeEvent(i: number): RoutingEvent { + return { + requestId: `bench-${i}`, + provider: i % 2 === 0 ? "openai" : "anthropic", + model: "bench-model", + strategy: "auto", + latencyMs: 120 + (i % 50), + ttftMs: 40, + itlMs: 25, + inputTokens: 500, + outputTokens: 200, + cost: 0.01, + retries: 0, + fallbackUsed: false, + outcome: i % 100 === 0 ? "malformed" : "success", + status: 200, + finishReason: "stop", + connectionId: null, + ts: Date.now(), + }; +} + +function bench(name: string, iterations: number, fn: (i: number) => number): void { + // Warmup + for (let i = 0; i < Math.min(10_000, iterations); i++) fn(i); + const start = performance.now(); + for (let i = 0; i < iterations; i++) fn(i); + const elapsedMs = performance.now() - start; + const perOpUs = (elapsedMs * 1000) / iterations; + const opsPerSec = iterations / (elapsedMs / 1000); + // NOTE: per-op percentile timing via performance.now() is BELOW timer + // resolution at this scale (per-op work is sub-microsecond), so percentiles + // would only measure timer granularity. Aggregate µs/op + throughput are the + // honest metrics here. + console.log( + `${name.padEnd(46)} ${iterations.toLocaleString()} ops in ${elapsedMs.toFixed(1)}ms | ` + + `${perOpUs.toFixed(3)}µs/op | ${Math.round(opsPerSec).toLocaleString()} ops/s` + ); +} + +// Shared sink set for the "event" and "otel" scenarios. +const store = new MemoryRoutingEventStore(500); +registerRoutingEventSink(store); +const qualitySink: RoutingEventSink = { + name: "quality", + record: (e) => recordQualityEvent(e), +}; +registerRoutingEventSink(qualitySink); + +// OTel sink that only enqueues (flush interval set absurdly high; never fires in-run). +const otelSink = new OtlpHttpsEventSink({ + endpoint: "http://127.0.0.1:1", // unreachable; record() never touches the network + flushIntervalMs: 1_000_000, +}); +registerRoutingEventSink(otelSink); + +const candidate = (quality: number): ProviderCandidate => ({ + provider: "p", + model: "m", + quotaRemaining: 100, + quotaTotal: 100, + circuitBreakerState: "CLOSED", + costPer1MTokens: 1, + p95LatencyMs: 100, + latencyStdDev: 10, + errorRate: 0, + quality, +}); +const pool = [candidate(0.9), candidate(0.5), candidate(0.2)]; + +console.log( + `\nRouting events benchmark (${N.toLocaleString()} iterations, 2 sinks + otel-enqueue)\n` +); + +// baseline: the scoring/decision cost the router already pays WITHOUT the event system. +bench("baseline: calculateFactors+Score", N, (i) => { + const c = pool[i % pool.length]; + const f = calculateFactors(c, pool, "general", () => 0.5); + return calculateScore(f, DEFAULT_WEIGHTS); +}); + +// baseline + event: the production hot-path cost (dispatch to memory+quality sinks). +bench("baseline + RoutingEvent (2 sinks)", N, (i) => { + const c = pool[i % pool.length]; + const f = calculateFactors(c, pool, "general", () => 0.5); + const score = calculateScore(f, DEFAULT_WEIGHTS); + dispatchRoutingEvent(makeEvent(i)); + return score; +}); + +// baseline + event + OTel-enqueue: adds the third sink (still no network I/O). +bench("baseline + event + OTel enqueue", N, (i) => { + const c = pool[i % pool.length]; + const f = calculateFactors(c, pool, "general", () => 0.5); + const score = calculateScore(f, DEFAULT_WEIGHTS); + dispatchRoutingEvent(makeEvent(i)); + return score; +}); + +// Concurrency: bursts interleaved on the event loop. +async function benchConcurrent(name: string, fn: () => number): Promise { + const bursts = 8; + const perBurst = Math.ceil(N / bursts); + const start = performance.now(); + await Promise.all( + Array.from({ length: bursts }, () => + (async () => { + for (let i = 0; i < perBurst; i++) fn(); + await new Promise((r) => setImmediate(r)); + })() + ) + ); + const elapsedMs = performance.now() - start; + const totalOps = bursts * perBurst; + console.log( + `${name.padEnd(46)} ${totalOps.toLocaleString()} ops in ${elapsedMs.toFixed(1)}ms ` + + `(${(elapsedMs * 1000) / totalOps}µs/op aggregate)` + ); +} + +console.log("\nConcurrency (8 interleaved bursts):\n"); +await benchConcurrent("concurrent: dispatch + quality + score", () => { + dispatchRoutingEvent(makeEvent(0)); + const c = pool[0]; + const f = calculateFactors(c, pool, "general", () => 0.5); + return calculateScore(f, DEFAULT_WEIGHTS); +}); + +console.log(`\nOTel sink stats: ${JSON.stringify(otelSink.getStats())}`); +otelSink.stop(); +console.log("(OTel buffer flushed; dropped events reflect the unreachable endpoint)\n"); diff --git a/src/app/api/v1/explain/routing/route.ts b/src/app/api/v1/explain/routing/route.ts new file mode 100644 index 0000000000..313bcf27b5 --- /dev/null +++ b/src/app/api/v1/explain/routing/route.ts @@ -0,0 +1,72 @@ +/** + * GET /v1/explain/routing — routing explainability + feedback state. + * + * Returns the most recent routing events (bounded in-memory ring buffer) and + * the per-provider/model quality snapshot produced by the feedback foundation + * (open-sse/services/routing). This is REAL decision data — the events were + * emitted by the request hot path, not recomputed after the fact. + * + * Safety: only routing metadata (provider/model/strategy/timing/tokens/outcome/ + * status/finish_reason). Never prompts, bodies, headers, credentials, accounts. + * + * Auth mirrors /v1/combos: valid Bearer API key or dashboard session. With + * REQUIRE_API_KEY=false (single-user local deployments) anonymous read is + * allowed, matching /v1/models behavior. + */ +import { NextResponse } from "next/server"; +import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; +import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; +import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; +import { isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; +import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags"; +import { + recentRoutingEvents, + routingQualitySnapshot, + routingOtelStats, + initRoutingObservability, + classifyQuality, +} from "@omniroute/open-sse/services/routing/index.ts"; + +export async function OPTIONS() { + return new Response(null, { + headers: { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "*", + }, + }); +} + +export async function GET(request: Request) { + const apiKeyRaw = extractApiKey(request); + const apiKeyOk = apiKeyRaw ? await isValidApiKey(apiKeyRaw) : false; + const dashboardOk = !apiKeyOk ? await isDashboardSessionAuthenticated(request) : false; + + if (!apiKeyOk && !dashboardOk && isRequireApiKeyEnabled()) { + return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Authentication required"); + } + + try { + const limit = Math.min( + 500, + Math.max(1, Number(new URL(request.url).searchParams.get("limit")) || 50) + ); + const { sinks, otelEnabled } = initRoutingObservability(); + const quality = routingQualitySnapshot(limit).map((q) => ({ + ...q, + classification: classifyQuality(q), + })); + return NextResponse.json( + { + object: "routing_explain", + sinks, + otelEnabled, + events: recentRoutingEvents(limit), + quality, + otel: routingOtelStats(), + }, + { headers: { "Cache-Control": "no-store" } } + ); + } catch { + return errorResponse(HTTP_STATUS.SERVER_ERROR, "Failed to build routing explain payload"); + } +} diff --git a/src/lib/usage/comboScoringInspector.ts b/src/lib/usage/comboScoringInspector.ts index 764f7ed0fb..54f97cd214 100644 --- a/src/lib/usage/comboScoringInspector.ts +++ b/src/lib/usage/comboScoringInspector.ts @@ -84,6 +84,7 @@ const FACTOR_KEYS: ComboScoringInspectorFactorKey[] = [ "sessionAvailability", "resetWindowAffinity", "connectionDensity", + "quality", ]; function roundNumber(value: number, digits = 4): number { @@ -315,14 +316,21 @@ function factorBreakdown( weights: ScoringWeights, context: CandidateContext ): ComboScoringInspectorFactor[] { - return FACTOR_KEYS.map((key) => ({ - key, - value: roundNumber(factors[key]), - weight: roundNumber(weights[key]), - contribution: roundNumber(factors[key] * weights[key]), - source: context.sources[key] ?? "default", - note: context.notes[key], - })).sort((left, right) => Math.abs(right.contribution) - Math.abs(left.contribution)); + return FACTOR_KEYS.map((key) => { + // Optional factors (cacheAffinity/sessionAvailability/quality) default to + // their scoring neutral (1 for a factor, 0 for a weight) so the contribution + // sum stays consistent with calculateScore. + const value = factors[key] ?? 1; + const weight = weights[key] ?? 0; + return { + key, + value: roundNumber(value), + weight: roundNumber(weight), + contribution: roundNumber(value * weight), + source: context.sources[key] ?? "default", + note: context.notes[key], + }; + }).sort((left, right) => Math.abs(right.contribution) - Math.abs(left.contribution)); } function targetForecastMap(targets: ComboForecastTarget[]): Map { diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 0ae41d9532..37174092b2 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -216,6 +216,8 @@ export function isLocalProvider(providerId: unknown): boolean { } export const SELF_HOSTED_CHAT_PROVIDER_IDS = new Set([ + "mlx-gemma", + "mlx-qwen", "ollama-local", "lm-studio", "vllm", @@ -272,6 +274,8 @@ export function providerAllowsOptionalApiKey(providerId: unknown): boolean { const BULK_API_KEY_EXCLUDED = new Set([ "vertex", "vertex-partner", + "mlx-gemma", + "mlx-qwen", "ollama-local", "grok-web", "perplexity-web", diff --git a/src/shared/constants/providers/local.ts b/src/shared/constants/providers/local.ts index a16939fbb9..a3e455d64f 100644 --- a/src/shared/constants/providers/local.ts +++ b/src/shared/constants/providers/local.ts @@ -3,6 +3,32 @@ * Pure data literal; re-exported by the providers.ts barrel. No behavior change. */ export const LOCAL_PROVIDERS = { + "mlx-gemma": { + id: "mlx-gemma", + alias: "mlx-gemma", + name: "MLX Gemma 26B", + icon: "memory", + color: "#8B5CF6", + textIcon: "MG", + website: "https://github.com/ml-explore/mlx", + authHint: + "No API key required. Runs mlx-lm server locally on port 11435. Requires uv and mlx-lm installed. Model: mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned (~15.9GB peak memory).", + localDefault: "http://localhost:11435/v1", + passthroughModels: false, + }, + "mlx-qwen": { + id: "mlx-qwen", + alias: "mlx-qwen", + name: "MLX Qwen 3.8 27B", + icon: "memory", + color: "#EC4899", + textIcon: "MQ", + website: "https://github.com/ml-explore/mlx", + authHint: + "No API key required. Runs mlx-lm server locally on port 11436. Requires uv and mlx-lm installed. Model: maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw (~13.1GB peak memory).", + localDefault: "http://localhost:11436/v1", + passthroughModels: false, + }, "ollama-local": { id: "ollama-local", alias: "ollama", diff --git a/src/shared/types/utilization.ts b/src/shared/types/utilization.ts index 20ec1850d1..4b637e7385 100644 --- a/src/shared/types/utilization.ts +++ b/src/shared/types/utilization.ts @@ -272,7 +272,8 @@ export type ComboScoringInspectorFactorKey = | "cacheAffinity" | "sessionAvailability" | "resetWindowAffinity" - | "connectionDensity"; + | "connectionDensity" + | "quality"; export type ComboScoringInspectorSource = "combo_health" | "combo_forecast" | "combo_autopilot" | "runtime" | "default"; diff --git a/tests/unit/auto-combo-scoring-clamp.test.ts b/tests/unit/auto-combo-scoring-clamp.test.ts index 1e00aca021..a73d24cf8b 100644 --- a/tests/unit/auto-combo-scoring-clamp.test.ts +++ b/tests/unit/auto-combo-scoring-clamp.test.ts @@ -36,6 +36,7 @@ const ONES: ScoringFactors = { contextAffinity: 1, resetWindowAffinity: 1, connectionDensity: 1, + quality: 1, }; function candidate(partial: Partial = {}): ProviderCandidate { diff --git a/tests/unit/mlx-provider.test.ts b/tests/unit/mlx-provider.test.ts new file mode 100644 index 0000000000..341717e46d --- /dev/null +++ b/tests/unit/mlx-provider.test.ts @@ -0,0 +1,57 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert"; +import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; + +// Mock fetch for health checks +const originalFetch = global.fetch; + +describe("MLX Provider Registry Entries", () => { + beforeEach(() => { + global.fetch = async () => ({ ok: false, status: 500 }); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("should have mlx-gemma registry entry with correct configuration", async () => { + const entry = getRegistryEntry("mlx-gemma"); + + assert.ok(entry, "mlx-gemma should be registered"); + assert.equal(entry?.id, "mlx-gemma"); + assert.equal(entry?.alias, "mlx-gemma"); + assert.equal(entry?.format, "openai"); + assert.equal(entry?.executor, "default"); // Uses default executor for OpenAI-compatible + assert.equal(entry?.baseUrl, "http://localhost:11435/v1"); + assert.equal(entry?.modelsUrl, "http://localhost:11435/v1/models"); + assert.equal(entry?.passthroughModels, false); + assert.ok(entry?.models?.length === 1); + assert.equal(entry?.models?.[0]?.id, "mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned"); + assert.equal(entry?.models?.[0]?.toolCalling, true); + assert.equal(entry?.models?.[0]?.contextLength, 8192); + }); + + it("should have mlx-qwen registry entry with correct configuration", async () => { + const entry = getRegistryEntry("mlx-qwen"); + + assert.ok(entry, "mlx-qwen should be registered"); + assert.equal(entry?.id, "mlx-qwen"); + assert.equal(entry?.alias, "mlx-qwen"); + assert.equal(entry?.format, "openai"); + assert.equal(entry?.executor, "default"); + assert.equal(entry?.baseUrl, "http://localhost:11436/v1"); + assert.equal(entry?.modelsUrl, "http://localhost:11436/v1/models"); + assert.equal(entry?.passthroughModels, false); + assert.ok(entry?.models?.length === 1); + assert.equal(entry?.models?.[0]?.id, "maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw"); + assert.equal(entry?.models?.[0]?.toolCalling, true); + assert.equal(entry?.models?.[0]?.contextLength, 8192); + }); + + it("should have both MLX providers in registered providers list", async () => { + const { getRegisteredProviders } = await import("../../open-sse/config/providerRegistry.ts"); + const providers = getRegisteredProviders(); + assert.ok(providers.includes("mlx-gemma")); + assert.ok(providers.includes("mlx-qwen")); + }); +}); diff --git a/tests/unit/routing-adaptive-e2e.test.ts b/tests/unit/routing-adaptive-e2e.test.ts new file mode 100644 index 0000000000..2d71bb289e --- /dev/null +++ b/tests/unit/routing-adaptive-e2e.test.ts @@ -0,0 +1,199 @@ +/** + * tests/unit/routing-adaptive-e2e.test.ts + * + * Deterministic end-to-end adaptive routing test (Phases 5 + 13). + * + * Exercises the REAL production routing path: the routing-event quality tracker + * → auto-combo scoring (`scoreAutoTargets` from open-sse/services/combo/autoStrategy.ts), + * which is what an "auto" combo uses to pick its preferred provider/model. + * + * Scenarios verified: + * 1. Healthy provider A outranks/ties backup B. + * 2. Degradation injected into A (sustained 5xx) → A's score falls below B → B preferred. + * 3. Recovery injected into A (sustained successes) → A's score recovers → A preferred again. + * 4. A cold provider C is neutral — it neither dominates nor is unfairly penalized. + * 5. One isolated failure does not overturn a healthy provider. + * + * The whole loop is deterministic: no network, no DB, only the real tracker + + * the real scorer. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + resetQualityTracker, + recordQualityEvent, +} from "../../open-sse/services/routing/quality.ts"; +import { qualityScoreFor } from "../../open-sse/services/routing/index.ts"; +import { scoreAutoTargets } from "../../open-sse/services/combo/autoStrategy.ts"; +import { DEFAULT_WEIGHTS } from "../../open-sse/services/autoCombo/scoring.ts"; +import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts"; +import type { AutoProviderCandidate } from "../../open-sse/services/combo/types.ts"; + +function target(provider: string, model: string, weight = 1): ResolvedComboTarget { + const modelStr = `${provider}/${model}`; + return { + kind: "model", + stepId: modelStr, + executionKey: modelStr, + modelStr, + provider, + providerId: provider, + connectionId: null, + weight, + label: null, + }; +} + +function candidate( + target: ResolvedComboTarget, + quality: number, + extra: Partial = {} +): AutoProviderCandidate { + return { + stepId: target.stepId, + executionKey: target.executionKey, + modelStr: target.modelStr, + provider: target.provider, + model: target.modelStr.split("/")[1], + quotaRemaining: 100, + quotaTotal: 100, + circuitBreakerState: "CLOSED", + costPer1MTokens: 1, + p95LatencyMs: 100, + latencyStdDev: 10, + errorRate: 0, + accountTier: "standard", + quotaResetIntervalSecs: 86400, + contextAffinity: 0.5, + sessionAvailability: 1, + resetWindowAffinity: 0.5, + connectionPoolSize: 1, + connectionId: null, + quality, + ...extra, + }; +} + +function record( + provider: string, + model: string, + partial: Partial[0]> = {} +): void { + recordQualityEvent({ + provider, + model, + outcome: "success", + status: 200, + latencyMs: 100, + finishReason: "stop", + outputTokens: 5, + ...partial, + }); +} + +function bestProvider(targets: ReturnType): string { + return targets[0].target.provider; +} + +/** Derive the bare model id from a "provider/model" modelStr. */ +function modelOf(t: ResolvedComboTarget): string { + return t.modelStr.slice(t.provider.length + 1); +} + +/** Score a set of providers using their live quality-tracker signal. */ +function scoreProviders( + ts: ResolvedComboTarget[], + weights = DEFAULT_WEIGHTS +): ReturnType { + return scoreAutoTargets( + ts, + ts.map((t) => candidate(t, qualityScoreFor(t.provider, modelOf(t)))), + "general", + weights + ); +} + +test("healthy provider A is preferred over backup B, cold C stays neutral", () => { + resetQualityTracker(); + // Warm A to high confidence with solid success. + for (let i = 0; i < 100; i++) record("a", "m"); + // B warm but mildly degraded. + for (let i = 0; i < 100; i++) + record("b", "m", { + outcome: i % 5 === 0 ? "error" : "success", + status: i % 5 === 0 ? 500 : 200, + }); + + const a = target("a", "m"); + const b = target("b", "m"); + const c = target("c", "m"); + const scored = scoreProviders([a, b, c]); + + assert.equal(bestProvider(scored), "a", "healthy A must be the top pick"); + // Cold C must not be top (neutral 0.5 quality vs A's high quality). + assert.notEqual(bestProvider(scored), "c", "cold provider must not dominate"); + // C's quality must be exactly neutral. + assert.equal(qualityScoreFor("c", "m"), 0.5); +}); + +test("degradation injected into A flips preference to B; recovery flips it back", () => { + resetQualityTracker(); + // Phase 0: A and B are otherwise identical; both healthy. A is preferred via + // stable tie-break, and quality is the only differentiator. + for (let i = 0; i < 100; i++) record("a", "m"); + for (let i = 0; i < 100; i++) record("b", "m"); + + const a = target("a", "m"); + const b = target("b", "m"); + const score = () => scoreProviders([a, b]); + + const initial = score(); + assert.equal(bestProvider(initial), "a", "initially A is preferred (tie-break on equal quality)"); + + // Phase 1: degrade A — sustained 5xx. A's quality collapses to ~0, so B + // (identical but healthy) becomes preferred. Gradual: the EWMA smooths the drop. + for (let i = 0; i < 60; i++) record("a", "m", { outcome: "error", status: 500 }); + const during = score(); + assert.equal( + bestProvider(during), + "b", + "sustained degradation must flip preference to B (gradual, not instant)" + ); + const qualityA = qualityScoreFor("a", "m"); + assert.ok(qualityA < 0.5, `A quality degraded below neutral, got ${qualityA}`); + + // Phase 2: recover A — sustained successes. Quality recovers and A's + // preference is restored. + for (let i = 0; i < 200; i++) record("a", "m"); + const after = score(); + assert.equal(bestProvider(after), "a", "recovery must restore A's preference"); + + // Phase 3: one isolated failure must not destroy A — its quality stays healthy + // (well above neutral), it just falls marginally behind the now-equally-tied B. + record("a", "m", { outcome: "error", status: 500 }); + const qualityAfterBlip = qualityScoreFor("a", "m"); + assert.ok( + qualityAfterBlip > 0.7, + `one isolated failure must not destroy A's quality, got ${qualityAfterBlip}` + ); + const afterBlip = score(); + const aEntry = afterBlip.find((s) => s.target.provider === "a"); + const bEntry = afterBlip.find((s) => s.target.provider === "b"); + assert.ok( + Math.abs(aEntry!.score - bEntry!.score) < 0.02, + "A must remain competitive after one isolated failure (not destroyed)" + ); +}); + +test("a provider with insufficient evidence does not dominate from optimistic init", () => { + resetQualityTracker(); + // Solid warm provider. + for (let i = 0; i < 200; i++) record("solid", "m"); + // Lucky cold provider: 7 flawless successes. + for (let i = 0; i < 7; i++) record("lucky", "m"); + + const s = target("solid", "m"); + const l = target("lucky", "m"); + const scored = scoreProviders([s, l]); + assert.equal(bestProvider(scored), "solid", "solid warm provider must beat a lucky cold one"); +}); diff --git a/tests/unit/routing-events-concurrency.test.ts b/tests/unit/routing-events-concurrency.test.ts new file mode 100644 index 0000000000..1eff7a1779 --- /dev/null +++ b/tests/unit/routing-events-concurrency.test.ts @@ -0,0 +1,172 @@ +/** + * tests/unit/routing-events-concurrency.test.ts + * + * Stress the routing-event system (Phase 4): + * - thousands of events through the real dispatch + quality + memory sinks + * - ring-buffer boundedness under sustained load (newest retained) + * - a throwing sink under load is isolated (other sinks keep working) + * - quality tracker updates stay consistent under interleaved async bursts + * - simultaneous reset() during inserts does not throw or corrupt state + * + * Node's event loop is single-threaded, so "concurrency" here is interleaved + * async execution; these tests assert correctness under bursts, not true + * parallelism. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + dispatchRoutingEvent, + MemoryRoutingEventStore, + registerRoutingEventSink, + clearRoutingEventSinks, + createRoutingEvent, + type RoutingEvent, + type RoutingEventSink, +} from "../../open-sse/services/routing/events.ts"; +import { + recordQualityEvent, + getQualityScore, + resetQualityTracker, + getProviderQuality, +} from "../../open-sse/services/routing/quality.ts"; + +function makeEvent(i: number): RoutingEvent { + return createRoutingEvent({ + requestId: `r-${i}`, + provider: i % 2 === 0 ? "openai" : "anthropic", + model: "m", + strategy: "auto", + latencyMs: 50 + (i % 100), + outcome: i % 100 === 0 ? "malformed" : "success", + status: 200, + finishReason: "stop", + outputTokens: 5, + }); +} + +test("sustained burst of thousands of events is bounded and consistent", async () => { + clearRoutingEventSinks(); + resetQualityTracker(); + const store = new MemoryRoutingEventStore(100); + registerRoutingEventSink(store); + registerRoutingEventSink({ + name: "quality", + record: (e) => recordQualityEvent(e), + }); + + const N = 10_000; + for (let i = 0; i < N; i++) dispatchRoutingEvent(makeEvent(i)); + + assert.equal(store.size, 100, "ring buffer must stay bounded at capacity"); + const recent = store.recent(5); + assert.equal(recent[0].requestId, `r-${N - 1}`, "newest event must be retained"); + + const q = getProviderQuality("openai", "m"); + assert.equal(q.samples, N / 2, "even-indexed events all landed in the quality tracker"); + assert.ok(q.operational > 0.5, "mostly-successful provider should be above neutral"); + + clearRoutingEventSinks(); +}); + +test("a throwing sink under load does not break other sinks", () => { + clearRoutingEventSinks(); + resetQualityTracker(); + const seen: string[] = []; + registerRoutingEventSink({ + name: "thrower", + record: () => { + throw new Error("sink boom"); + }, + }); + registerRoutingEventSink({ + name: "collector", + record: (e) => void seen.push(e.requestId), + }); + + for (let i = 0; i < 2000; i++) dispatchRoutingEvent(makeEvent(i)); + assert.equal(seen.length, 2000, "all events must still reach the good sink"); + clearRoutingEventSinks(); +}); + +test("interleaved async bursts keep quality math consistent", async () => { + resetQualityTracker(); + const bursts = Array.from({ length: 8 }, (_, b) => + (async () => { + for (let i = 0; i < 500; i++) { + recordQualityEvent(makeEvent(b * 500 + i)); + // Yield occasionally to interleave with the other bursts. + if (i % 50 === 0) await new Promise((r) => setImmediate(r)); + } + })() + ); + await Promise.all(bursts); + + const q = getProviderQuality("openai", "m"); + assert.equal(q.samples, 2000, "4 bursts * 500 with i%2==0 → 2000 openai samples"); + assert.ok(Number.isFinite(q.operational) && q.operational >= 0 && q.operational <= 1); + // Recency should be non-null and tiny (events were just recorded). + assert.ok(q.recencyMs !== null && q.recencyMs < 5000); +}); + +test("reset during inserts is safe and state re-initializes cleanly", async () => { + resetQualityTracker(); + const store = new MemoryRoutingEventStore(50); + registerRoutingEventSink(store); + registerRoutingEventSink({ name: "quality", record: (e) => recordQualityEvent(e) }); + + const writer = (async () => { + for (let i = 0; i < 2000; i++) { + dispatchRoutingEvent(makeEvent(i)); + if (i % 200 === 0) await new Promise((r) => setImmediate(r)); + } + })(); + + // Fire several resets while the writer is mid-flight. + const resets = Array.from({ length: 3 }, (_, k) => + (async () => { + await new Promise((r) => setImmediate(r)); + resetQualityTracker(); + store.clear(); + })() + ); + await Promise.all(resets); + await writer; + + // After a reset the tracker is empty for the reset epoch; post-reset events + // must still record without throwing. We assert non-negative, finite state. + const q = getProviderQuality("openai", "m"); + assert.ok(q.samples >= 0); + assert.ok(Number.isFinite(q.operational)); + clearRoutingEventSinks(); + resetQualityTracker(); +}); + +test("quality score never goes NaN or out of [0,1] under adversarial events", () => { + resetQualityTracker(); + const bad: RoutingEvent[] = [ + makeEvent(0), + createRoutingEvent({ + requestId: "nan-1", + provider: "nanp", + model: "nanm", + latencyMs: NaN, + outcome: "error", + status: NaN, + ttftMs: NaN, + outputTokens: NaN, + }), + ]; + for (const e of bad) dispatchRoutingEvent(makeEvent(1)); + recordQualityEvent({ + provider: "nanp", + model: "nanm", + outcome: "error", + status: NaN, + latencyMs: NaN, + ttftMs: NaN, + outputTokens: NaN, + }); + const score = getQualityScore("nanp", "nanm"); + assert.ok(Number.isFinite(score), `score must be finite, got ${score}`); + assert.ok(score >= 0 && score <= 1, `score in [0,1], got ${score}`); +}); diff --git a/tests/unit/routing-events.test.ts b/tests/unit/routing-events.test.ts new file mode 100644 index 0000000000..672e566664 --- /dev/null +++ b/tests/unit/routing-events.test.ts @@ -0,0 +1,141 @@ +/** + * tests/unit/routing-events.test.ts + * + * Routing feedback foundation (open-sse/services/routing/events.ts): + * - createRoutingEvent normalizes defaults + * - outcomeFromStatus classifies HTTP statuses + * - MemoryRoutingEventStore is bounded and returns newest-first + * - dispatchRoutingEvent fans out to sinks and isolates a throwing sink + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + MemoryRoutingEventStore, + createRoutingEvent, + outcomeFromStatus, + dispatchRoutingEvent, + registerRoutingEventSink, + listRoutingEventSinks, + clearRoutingEventSinks, + type RoutingEvent, + type RoutingEventSink, +} from "../../open-sse/services/routing/events.ts"; + +function event(partial: Partial = {}): RoutingEvent { + return createRoutingEvent({ + requestId: "req-1", + provider: "openai", + model: "gpt-4o", + strategy: "auto", + latencyMs: 120, + outcome: "success", + status: 200, + ...partial, + }); +} + +test("createRoutingEvent fills observability defaults", () => { + const e = createRoutingEvent({ + requestId: "req-x", + provider: "anthropic", + model: "claude-4", + latencyMs: 50, + outcome: "error", + status: 500, + }); + assert.equal(e.strategy, "direct"); + assert.equal(e.ttftMs, null); + assert.equal(e.inputTokens, null); + assert.equal(e.outputTokens, null); + assert.equal(e.cost, null); + assert.equal(e.retries, 0); + assert.equal(e.fallbackUsed, false); + assert.equal(e.finishReason, null); + assert.equal(e.connectionId, null); + assert.ok(e.ts > 0); + assert.equal(e.status, 500); +}); + +test("outcomeFromStatus classifies statuses", () => { + assert.equal(outcomeFromStatus(200), "success"); + assert.equal(outcomeFromStatus(201), "success"); + assert.equal(outcomeFromStatus(429), "rate_limited"); + assert.equal(outcomeFromStatus(408), "timeout"); + assert.equal(outcomeFromStatus(504), "timeout"); + assert.equal(outcomeFromStatus(500), "error"); + assert.equal(outcomeFromStatus(400), "error"); + assert.equal(outcomeFromStatus(null), "error"); + assert.equal(outcomeFromStatus(undefined), "error"); +}); + +test("MemoryRoutingEventStore returns newest-first within capacity", () => { + const store = new MemoryRoutingEventStore(5); + for (let i = 0; i < 5; i++) store.record(event({ requestId: `r-${i}` })); + const recent = store.recent(5); + assert.equal(recent.length, 5); + assert.equal(recent[0].requestId, "r-4"); + assert.equal(recent[4].requestId, "r-0"); +}); + +test("MemoryRoutingEventStore is bounded and still newest-first after overflow", () => { + const store = new MemoryRoutingEventStore(3); + for (let i = 0; i < 10; i++) store.record(event({ requestId: `r-${i}` })); + assert.equal(store.size, 3); + const recent = store.recent(3); + assert.deepEqual( + recent.map((e) => e.requestId), + ["r-9", "r-8", "r-7"] + ); + store.clear(); + assert.equal(store.size, 0); + assert.deepEqual(store.recent(), []); +}); + +test("dispatchRoutingEvent fans out to every registered sink", () => { + const seen: string[] = []; + const sink: RoutingEventSink = { + name: "test-a", + record: (e) => void seen.push(e.requestId), + }; + const unsub = registerRoutingEventSink(sink); + try { + dispatchRoutingEvent(event({ requestId: "fan-1" })); + dispatchRoutingEvent(event({ requestId: "fan-2" })); + assert.deepEqual(seen, ["fan-1", "fan-2"]); + } finally { + unsub(); + } +}); + +test("dispatchRoutingEvent isolates a throwing sink", () => { + const badSink: RoutingEventSink = { + name: "test-throw", + record: () => { + throw new Error("boom"); + }, + }; + const goodSeen: string[] = []; + const goodSink: RoutingEventSink = { + name: "test-good", + record: (e) => void goodSeen.push(e.requestId), + }; + registerRoutingEventSink(badSink); + registerRoutingEventSink(goodSink); + try { + dispatchRoutingEvent(event({ requestId: "isolated" })); + assert.deepEqual(goodSeen, ["isolated"]); + } finally { + clearRoutingEventSinks(); + } +}); + +test("listRoutingEventSinks reports registered names", () => { + clearRoutingEventSinks(); + assert.deepEqual(listRoutingEventSinks(), []); + const unsub = registerRoutingEventSink({ name: "probe", record: () => {} }); + try { + assert.deepEqual(listRoutingEventSinks(), ["probe"]); + } finally { + unsub(); + } +}); diff --git a/tests/unit/routing-otel.test.ts b/tests/unit/routing-otel.test.ts new file mode 100644 index 0000000000..1a68484f81 --- /dev/null +++ b/tests/unit/routing-otel.test.ts @@ -0,0 +1,129 @@ +/** + * tests/unit/routing-otel.test.ts + * + * Optional OpenTelemetry sink (open-sse/services/routing/otel.ts): + * - disabled unless an endpoint is configured + * - buildOtlpTracesPayload emits GenAI semantic-convention spans + * - record() enqueues without performing I/O; stop() flushes via fetch + * - dropped events are counted when the buffer overflows + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + buildOtlpTracesPayload, + isRoutingOtelEnabled, + OtlpHttpsEventSink, +} from "../../open-sse/services/routing/otel.ts"; +import type { RoutingEvent } from "../../open-sse/services/routing/events.ts"; + +function event(partial: Partial = {}): RoutingEvent { + return { + requestId: "req-1", + provider: "openai", + model: "gpt-4o", + strategy: "auto", + latencyMs: 120, + ttftMs: 40, + inputTokens: 10, + outputTokens: 20, + cost: 0.01, + retries: 1, + fallbackUsed: true, + outcome: "success", + status: 200, + finishReason: "stop", + connectionId: "conn-1", + ts: 1_700_000_000_000, + ...partial, + }; +} + +test("isRoutingOtelEnabled is false without an endpoint", () => { + assert.equal(isRoutingOtelEnabled({}), false); + assert.equal(isRoutingOtelEnabled({ OMNIROUTE_OTEL_ENDPOINT: " " }), false); +}); + +test("isRoutingOtelEnabled honors OMNIROUTE_OTEL_ENDPOINT and OTLP env", () => { + assert.equal(isRoutingOtelEnabled({ OMNIROUTE_OTEL_ENDPOINT: "http://collector:4318" }), true); + assert.equal( + isRoutingOtelEnabled({ OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318" }), + true + ); +}); + +test("buildOtlpTracesPayload emits GenAI semantic-convention spans", () => { + const payload = buildOtlpTracesPayload([event()], "omniroute-test") as { + resourceSpans: Array<{ + scopeSpans: Array<{ + spans: Array<{ + attributes: Array<{ key: string; value: { stringValue?: string; intValue?: string } }>; + }>; + }>; + }>; + }; + const span = payload.resourceSpans[0].scopeSpans[0].spans[0]; + const attrs = Object.fromEntries( + span.attributes.map((a) => [a.key, a.value.stringValue ?? a.value.intValue]) + ); + assert.equal(attrs["gen_ai.provider.name"], "openai"); + assert.equal(attrs["gen_ai.request.model"], "gpt-4o"); + assert.equal(attrs["gen_ai.system"], "auto"); + assert.equal(attrs["gen_ai.usage.input_tokens"], "10"); + assert.equal(attrs["gen_ai.usage.output_tokens"], "20"); + assert.equal(attrs["gen_ai.completion.finish_reason"], "stop"); + assert.equal(attrs["omniroute.routing.outcome"], "success"); + assert.equal(attrs["omniroute.routing.status"], "200"); + assert.equal(attrs["omniroute.routing.retries"], "1"); + assert.equal(attrs["omniroute.routing.fallback_used"], "1"); + assert.equal(attrs["omniroute.connection_id"], "conn-1"); + assert.ok(BigInt(span.startTimeUnixNano) > 0n); +}); + +test("OtlpHttpsEventSink record() enqueues without I/O and flush sends via fetch", async () => { + const calls: Array<{ url: string; body: string }> = []; + const originalFetch = global.fetch; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.fetch = (async (url: any, init: any) => { + calls.push({ url: String(url), body: String(init?.body ?? "") }); + return { ok: true } as Response; + }) as typeof fetch; + + const sink = new OtlpHttpsEventSink({ + endpoint: "http://collector:4318", + flushIntervalMs: 1_000_000, // effectively never auto-flush in the test + }); + try { + sink.record(event()); + sink.record(event({ requestId: "req-2" })); + assert.equal(sink.getStats().buffered, 2); + // Force an explicit flush via stop(). + await new Promise((r) => setTimeout(r, 20)); + sink.stop(); + await new Promise((r) => setTimeout(r, 50)); + assert.equal(calls.length, 1, "one flush should have been sent"); + assert.ok(calls[0].url.endsWith("/v1/traces"), calls[0].url); + const body = JSON.parse(calls[0].body); + assert.ok(body.resourceSpans[0].scopeSpans[0].spans.length === 2); + assert.equal(sink.getStats().buffered, 0); + } finally { + global.fetch = originalFetch; + } +}); + +test("OtlpHttpsEventSink drops oldest when the buffer is saturated", async () => { + const originalFetch = global.fetch; + global.fetch = (async () => ({ ok: true }) as Response) as typeof fetch; + const sink = new OtlpHttpsEventSink({ + endpoint: "http://collector:4318", + maxBatchSize: 2, + flushIntervalMs: 1_000_000, + }); + try { + for (let i = 0; i < 20; i++) sink.record(event({ requestId: `r-${i}` })); + const stats = sink.getStats(); + assert.ok(stats.dropped > 0, "overload must drop events, never block"); + sink.stop(); + } finally { + global.fetch = originalFetch; + } +}); diff --git a/tests/unit/routing-quality.test.ts b/tests/unit/routing-quality.test.ts new file mode 100644 index 0000000000..56197401f1 --- /dev/null +++ b/tests/unit/routing-quality.test.ts @@ -0,0 +1,187 @@ +/** + * tests/unit/routing-quality.test.ts + * + * Feedback-driven quality signal v2 (open-sse/services/routing/quality.ts): + * - operational vs semantic separation (semantic is NEVER manufactured from HTTP) + * - neutral 0.5 for cold providers (not unfairly penalized, cannot dominate) + * - confidence/sample-awareness (lucky cold provider cannot outrank a solid warm one) + * - success raises / failure lowers the EWMA score + * - malformed / stream-interrupted / empty-output anomalies penalize + * - 429 is transient (far lighter than a 500) + * - confidence ramps with sample count + * - reset clears state + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + recordQualityEvent, + getQualityScore, + getProviderQuality, + setSemanticQuality, + getQualitySnapshot, + resetQualityTracker, + QUALITY_WELL_KNOWN, +} from "../../open-sse/services/routing/quality.ts"; + +const { CONFIDENCE_FULL_SAMPLES } = QUALITY_WELL_KNOWN; + +function record( + provider: string, + model: string, + partial: Partial[0]> = {} +): void { + recordQualityEvent({ + provider, + model, + outcome: "success", + status: 200, + latencyMs: 100, + finishReason: "stop", + ...partial, + }); +} + +test("cold provider scores neutral 0.5 (no penalty, no dominance)", () => { + resetQualityTracker(); + assert.equal(getQualityScore("openai", "gpt-4o"), 0.5); + const q = getProviderQuality("openai", "gpt-4o"); + assert.equal(q.operational, 0.5); + assert.equal(q.confidence, 0); + assert.equal(q.samples, 0); +}); + +test("below warmup threshold the score is pulled toward neutral (not 1.0)", () => { + resetQualityTracker(); + // 7 lucky successes: operational EWMA → 1.0, but confidence is low, so the + // blended score must stay well below 1.0 — it must not dominate a solid warm provider. + for (let i = 0; i < 7; i++) record("openai", "gpt-4o"); + const lucky = getQualityScore("openai", "gpt-4o"); + assert.ok(lucky > 0.5 && lucky < 0.8, `lucky cold provider should be near-neutral, got ${lucky}`); +}); + +test("a provider with thousands of solid observations outranks a lucky cold provider", () => { + resetQualityTracker(); + // Solid warm provider: 4000 samples, ~91% success. + for (let i = 0; i < 4000; i++) { + record("p", "solid", { + outcome: i % 11 === 0 ? "error" : "success", + status: i % 11 === 0 ? 500 : 200, + }); + } + // Lucky cold provider: 7 samples, all success. + for (let i = 0; i < 7; i++) record("p", "lucky"); + const solid = getQualityScore("p", "solid"); + const lucky = getQualityScore("p", "lucky"); + assert.ok(solid > lucky, `solid (${solid}) must outrank lucky (${lucky})`); + assert.ok(solid > 0.8, `solid provider should score high, got ${solid}`); +}); + +test("sustained failures degrade; sustained successes recover gradually", () => { + resetQualityTracker(); + for (let i = 0; i < 20; i++) record("openai", "gpt-4o", { outcome: "error", status: 500 }); + const degraded = getQualityScore("openai", "gpt-4o"); + assert.ok(degraded < 0.4, `expected degraded score, got ${degraded}`); + + for (let i = 0; i < 40; i++) record("openai", "gpt-4o"); + const recovered = getQualityScore("openai", "gpt-4o"); + assert.ok(recovered > degraded, "successes must recover the score"); + assert.ok(recovered > 0.7, `expected recovery toward healthy, got ${recovered}`); +}); + +test("one isolated failure does not destroy a warm provider", () => { + resetQualityTracker(); + for (let i = 0; i < 100; i++) record("p", "m"); + const before = getQualityScore("p", "m"); + record("p", "m", { outcome: "error", status: 500 }); + const after = getQualityScore("p", "m"); + assert.ok(after > 0.7, `single failure must not destroy a healthy provider, got ${after}`); + assert.ok(after < before, "the single failure should still register"); +}); + +test("malformed and stream-interrupted outcomes penalize more than a clean error", () => { + resetQualityTracker(); + record("p", "m-a", { outcome: "malformed", status: 200, finishReason: "stop" }); + for (let i = 0; i < 20; i++) record("p", "m-a"); + + record("p", "m-b"); + for (let i = 0; i < 20; i++) record("p", "m-b"); + + assert.ok( + getQualityScore("p", "m-a") < getQualityScore("p", "m-b"), + "anomaly history must lower quality below a clean record" + ); +}); + +test("finish_reason=length (truncated output) counts as an anomaly", () => { + resetQualityTracker(); + for (let i = 0; i < 20; i++) + record("p", "truncated", { outcome: "success", finishReason: "length" }); + for (let i = 0; i < 20; i++) record("p", "clean"); + assert.ok( + getQualityScore("p", "truncated") < getQualityScore("p", "clean"), + "length finish_reason must hurt quality" + ); +}); + +test("zero-output successes count as anomalies; missing output does not", () => { + resetQualityTracker(); + for (let i = 0; i < 20; i++) + record("p", "empty", { outcome: "success", outputTokens: 0, finishReason: "stop" }); + for (let i = 0; i < 20; i++) record("p", "ok", { outcome: "success", outputTokens: 5 }); + assert.ok( + getQualityScore("p", "empty") < getQualityScore("p", "ok"), + "zero-output 200 must hurt quality more than a normal 200" + ); +}); + +test("429 is transient (near-neutral), not a quality failure", () => { + resetQualityTracker(); + for (let i = 0; i < 50; i++) record("p", "rl", { outcome: "rate_limited", status: 429 }); + for (let i = 0; i < 50; i++) record("p", "err", { outcome: "error", status: 500 }); + const rateLimited = getQualityScore("p", "rl"); + const error = getQualityScore("p", "err"); + assert.ok(rateLimited > error, "rate-limited should score better than hard failures"); + assert.ok(rateLimited >= 0.45, "rate-limit alone should not tank quality below neutral"); +}); + +test("semantic quality is separate from operational and never manufactured", () => { + resetQualityTracker(); + // A provider with perfect operational history but no evaluator → semantic null. + for (let i = 0; i < 100; i++) record("p", "op-only"); + const q = getProviderQuality("p", "op-only"); + assert.equal(q.semantic, null, "semantic must be null until an evaluator provides it"); + assert.ok(q.operational > 0.9, "operational can be high independently"); + + // An evaluator can then attach a semantic score. + setSemanticQuality("p", "op-only", 0.42, 0.8); + const q2 = getProviderQuality("p", "op-only"); + assert.equal(q2.semantic, 0.42); + assert.equal(q2.semanticConfidence, 0.8); + // The operational score must NOT be contaminated by the semantic score. + assert.ok( + Math.abs(q2.operational - q.operational) < 1e-9, + "semantic must not leak into operational" + ); +}); + +test("snapshot reports confidence, samples and anomaly counts", () => { + resetQualityTracker(); + for (let i = 0; i < 10; i++) record("snap", "model"); + record("snap", "model", { outcome: "malformed" }); + const snap = getQualitySnapshot(); + const view = snap.find((v) => v.provider === "snap" && v.model === "model"); + assert.ok(view, "snapshot must contain the tracked model"); + assert.equal(view!.confidence, 11 / CONFIDENCE_FULL_SAMPLES); + assert.ok(view!.samples === 11); + assert.ok(view!.anomalies >= 1); + assert.ok(view!.operational >= 0 && view!.operational <= 1); +}); + +test("reset clears all tracked state", () => { + resetQualityTracker(); + record("p", "m"); + assert.equal(getQualitySnapshot().length, 1); + resetQualityTracker(); + assert.equal(getQualitySnapshot().length, 0); + assert.equal(getQualityScore("p", "m"), 0.5); +}); diff --git a/tests/unit/routing-scoring-quality.test.ts b/tests/unit/routing-scoring-quality.test.ts new file mode 100644 index 0000000000..974ede1c51 --- /dev/null +++ b/tests/unit/routing-scoring-quality.test.ts @@ -0,0 +1,96 @@ +/** + * tests/unit/routing-scoring-quality.test.ts + * + * Scoring integration of the feedback quality signal: + * - DEFAULT_WEIGHTS still sums to ~1.0 (validateWeights) with the new quality weight + * - calculateFactors defaults missing quality to neutral 1.0 + * - calculateScore applies the quality factor + * - a low-quality candidate ranks below an identical high-quality one + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + calculateFactors, + calculateScore, + DEFAULT_WEIGHTS, + normalizeScoringWeights, + validateWeights, + type ProviderCandidate, + type ScoringFactors, +} from "../../open-sse/services/autoCombo/scoring.ts"; + +function candidate(partial: Partial = {}): ProviderCandidate { + return { + provider: "p", + model: "m", + quotaRemaining: 100, + quotaTotal: 100, + circuitBreakerState: "CLOSED", + costPer1MTokens: 1, + p95LatencyMs: 100, + latencyStdDev: 10, + errorRate: 0, + accountTier: "standard", + quotaResetIntervalSecs: 86400, + ...partial, + }; +} + +test("DEFAULT_WEIGHTS sums to ~1 with the new quality weight", () => { + const sum = Object.values(DEFAULT_WEIGHTS).reduce((a, b) => a + Number(b), 0); + assert.ok(Math.abs(sum - 1) < 1e-9, `expected sum ≈ 1, got ${sum}`); + assert.ok(validateWeights(DEFAULT_WEIGHTS), "validateWeights must accept DEFAULT_WEIGHTS"); + assert.ok((DEFAULT_WEIGHTS.quality ?? 0) > 0, "quality weight must be > 0"); +}); + +test("calculateFactors defaults missing quality to neutral 0.5", () => { + const factors = calculateFactors(candidate(), [candidate()], "general", () => 0.5); + assert.equal(factors.quality, 0.5); +}); + +test("calculateFactors clamps quality to [0,1]", () => { + const low = calculateFactors(candidate({ quality: -2 }), [candidate()], "general", () => 0.5); + assert.equal(low.quality, 0); + const high = calculateFactors(candidate({ quality: 5 }), [candidate()], "general", () => 0.5); + assert.equal(high.quality, 1); +}); + +test("calculateScore applies the quality factor", () => { + const base: ScoringFactors = { + quota: 0.5, + health: 0.5, + costInv: 0.5, + latencyInv: 0.5, + taskFit: 0.5, + stability: 0.5, + tierPriority: 0.5, + tierAffinity: 0.5, + specificityMatch: 0.5, + contextAffinity: 0.5, + resetWindowAffinity: 0.5, + connectionDensity: 0.5, + }; + const good = calculateScore({ ...base, quality: 1 }, DEFAULT_WEIGHTS); + const bad = calculateScore({ ...base, quality: 0 }, DEFAULT_WEIGHTS); + assert.ok(good > bad, "higher quality must score strictly higher"); + assert.ok(good >= 0 && good <= 1); + assert.ok(bad >= 0 && bad <= 1); +}); + +test("low-quality candidate ranks below identical high-quality candidate", () => { + const good = candidate({ provider: "p", model: "good", quality: 1 }); + const poor = candidate({ provider: "p", model: "poor", quality: 0.3 }); + const pool = [good, poor]; + const fg = calculateFactors(good, pool, "general", () => 0.5); + const fp = calculateFactors(poor, pool, "general", () => 0.5); + const sg = calculateScore(fg, DEFAULT_WEIGHTS); + const sp = calculateScore(fp, DEFAULT_WEIGHTS); + assert.ok(sg > sp, `good candidate (${sg}) must outrank poor (${sp})`); +}); + +test("normalizeScoringWeights keeps quality and renormalizes to 1", () => { + const normalized = normalizeScoringWeights({ quality: 0.1 }); + const total = Object.values(normalized).reduce((s, v) => s + Number(v), 0); + assert.ok(Math.abs(total - 1) < 1e-9); + assert.ok((normalized.quality ?? 0) > 0); +}); diff --git a/tests/unit/stream-timing.test.ts b/tests/unit/stream-timing.test.ts new file mode 100644 index 0000000000..b6c5782579 --- /dev/null +++ b/tests/unit/stream-timing.test.ts @@ -0,0 +1,86 @@ +/** + * tests/unit/stream-timing.test.ts + * + * Canonical stream instrumentation (open-sse/utils/streamTiming.ts): + * - TTFT = first-forwarded-SSE-chunk latency (NOT token-level) — documented + * - ITL = mean inter-chunk gap (chunk-latency proxy) + * - first-byte vs first-forward distinction + * - interruption marking + * - malformed/empty chunks do not corrupt timing + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createStreamTiming, type StreamTiming } from "../../open-sse/utils/streamTiming.ts"; + +test("ttft() is null when nothing was forwarded", () => { + const t = createStreamTiming(); + t.markByte(); + assert.equal(t.ttftMs(), null); + assert.equal(t.avgItlMs(), null); +}); + +test("ttft() measures first-forwarded-chunk latency (byte vs forward distinguished)", async () => { + const t = createStreamTiming(); + t.markByte(); // first upstream byte arrives immediately + await new Promise((r) => setTimeout(r, 20)); + t.markForward(); // first chunk forwarded 20ms later + const ttft = t.ttftMs(); + assert.ok(ttft !== null && ttft >= 20 && ttft < 5000, `ttft=${ttft}`); + assert.ok(t.firstByteAt !== null); + assert.ok(t.firstByteAt! < t.firstForwardAt!, "first byte precedes first forward"); +}); + +test("avgItlMs() measures mean inter-chunk gap across multiple chunks", async () => { + const t = createStreamTiming(); + for (let i = 0; i < 4; i++) { + t.markForward(); + await new Promise((r) => setTimeout(r, 10)); + } + const itl = t.avgItlMs(); + assert.ok(itl !== null && itl >= 8 && itl < 5000, `itl=${itl}`); + assert.equal(t.forwardedChunks, 4); +}); + +test("empty chunks do not corrupt timing (markByte without forward)", () => { + const t = createStreamTiming(); + t.markByte(); + t.markByte(); // duplicate bytes are idempotent for first-byte + assert.equal(t.ttftMs(), null, "no forward → no ttft"); + t.markForward(); + assert.ok(t.ttftMs() !== null); +}); + +test("malformed/keepalive-only traffic (no forward) yields no ttft", () => { + const t = createStreamTiming(); + // Simulate a provider that only sends keepalives/blank lines, never data. + for (let i = 0; i < 5; i++) t.markByte(); + assert.equal(t.ttftMs(), null); + assert.equal(t.forwardedChunks, 0); +}); + +test("interruption is recorded and does not reset other timing", async () => { + const t = createStreamTiming(); + t.markForward(); + await new Promise((r) => setTimeout(r, 5)); + t.markForward(); + t.markInterrupted(); + assert.equal(t.interrupted, true); + assert.ok(t.ttftMs() !== null); + assert.ok(t.avgItlMs() !== null); +}); + +test("normal completion: totalMs() is monotonic and >= first-forward latency", async () => { + const t = createStreamTiming(); + await new Promise((r) => setTimeout(r, 15)); + t.markForward(); + const total = t.totalMs(); + const ttft = t.ttftMs(); + assert.ok(total >= 15); + assert.ok(ttft !== null && ttft <= total, "ttft must be <= total duration"); +}); + +test("max inter-chunk samples are bounded (memory bound)", async () => { + const t = createStreamTiming(); + for (let i = 0; i < 200; i++) t.markForward(); + assert.ok(t.interChunkGaps.length <= 32, `bounded to 32 samples, got ${t.interChunkGaps.length}`); +}); From 871832820f5dc0eeba72259f7e673193d9058454 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 17:28:37 -0300 Subject: [PATCH 110/135] fix(memory): enable agent memory save via MCP tools + builtin stream guard (#10887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — clean single-commit extraction from #9115's genuinely new content (see PR body for the full extraction rationale: 66-commit branch, only 1 commit matched the stated scope). typecheck/file-size/changelog gates clean, 19/19 unit + 14/14 integration tests passing. --- changelog.d/fixes/10887-memory-mcp-tools.md | 1 + open-sse/handlers/chatCore.ts | 9 +- .../chatCore/memorySkillsInjection.ts | 38 +++ open-sse/mcp-server/tools/memoryTools.ts | 29 +- src/lib/skills/interception.ts | 36 ++- src/lib/skills/memoryBuiltins.ts | 294 ++++++++++++++++++ tests/integration/memory-pipeline.test.ts | 68 ++++ .../chatcore-memory-skills-injection.test.ts | 116 +++++++ tests/unit/skills-memory-builtins.test.ts | 227 ++++++++++++++ 9 files changed, 801 insertions(+), 17 deletions(-) create mode 100644 changelog.d/fixes/10887-memory-mcp-tools.md create mode 100644 src/lib/skills/memoryBuiltins.ts create mode 100644 tests/unit/skills-memory-builtins.test.ts diff --git a/changelog.d/fixes/10887-memory-mcp-tools.md b/changelog.d/fixes/10887-memory-mcp-tools.md new file mode 100644 index 0000000000..8dc02db1d9 --- /dev/null +++ b/changelog.d/fixes/10887-memory-mcp-tools.md @@ -0,0 +1 @@ +- **fix(memory):** enable agent memory save/update via MCP tools (`memory_save`/`update`/`search`/`delete` builtins with per-provider schemas, `apiKeyId` optional with caller-principal fallback) and gate server-side memory builtin injection to non-stream requests only ([#10887](https://github.com/diegosouzapw/OmniRoute/pull/10887)) — thanks @Egorich-print diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index fa6085ad19..c40bca8296 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -432,6 +432,7 @@ import { isLocalStreamLifecycleError } from "@/shared/utils/circuitBreaker"; import { shouldIsolateProbeFailures } from "@/shared/utils/probeOrigin"; import { extractFacts } from "@/lib/memory/extraction"; import { handleToolCallExecution } from "@/lib/skills/interception"; +import { MEMORY_BUILTIN_TOOL_NAMES } from "@/lib/skills/memoryBuiltins"; import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers"; import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults"; import { @@ -4952,9 +4953,11 @@ export async function handleChatCore({ const customSkillExecutionEnabled = Boolean(memoryOwnerId) && memorySettings?.skillsEnabled === true; - const builtinToolNames = [webSearchFallbackPlan.toolName, webFetchFallbackPlan.toolName].filter( - (name): name is string => Boolean(name) - ); + const builtinToolNames = [ + webSearchFallbackPlan.toolName, + webFetchFallbackPlan.toolName, + ...(memoryOwnerId && memorySettings?.enabled ? MEMORY_BUILTIN_TOOL_NAMES : []), + ].filter((name): name is string => Boolean(name)); if (customSkillExecutionEnabled || builtinToolNames.length > 0) { const skillSessionId = pipelineSessionId; diff --git a/open-sse/handlers/chatCore/memorySkillsInjection.ts b/open-sse/handlers/chatCore/memorySkillsInjection.ts index 2a43c17cbf..14fbdc9575 100644 --- a/open-sse/handlers/chatCore/memorySkillsInjection.ts +++ b/open-sse/handlers/chatCore/memorySkillsInjection.ts @@ -2,6 +2,7 @@ import { retrieveMemories } from "@/lib/memory/retrieval"; import { getMemorySettings, DEFAULT_MEMORY_SETTINGS, toMemoryRetrievalConfig } from "@/lib/memory/settings"; import { injectMemory, shouldInjectMemory } from "@/lib/memory/injection"; import { injectSkills } from "@/lib/skills/injection"; +import { buildMemoryToolsForProvider } from "@/lib/skills/memoryBuiltins"; import { skillRegistry } from "@/lib/skills/registry"; import { FORMATS } from "../../translator/formats.ts"; import { detectCachingContext } from "../../services/compression/cachingAware.ts"; @@ -138,6 +139,43 @@ export async function injectMemoryAndSkills({ } } + if (memoryOwnerId && memorySettings?.enabled && body.stream !== true) { + // Server-side builtin memory tools (memory_save/update/search/delete) are + // executed by the gateway's tool-call interception, which runs only on the + // non-stream path. Stream clients (opencode etc.) execute tools client-side, + // so for them these tools would be announced but never executed; they should + // use the MCP memory tools (omniroute_memory_*) instead. + const existingTools = Array.isArray(body.tools) ? body.tools : []; + const existingToolNames = new Set( + existingTools.flatMap((tool) => { + const record = tool as Record | null; + if (!record || typeof record !== "object") return []; + const fn = record.function as Record | undefined; + if (typeof fn?.name === "string") return [fn.name]; + if (typeof record.name === "string") return [record.name]; + return []; + }) + ); + const memoryTools = buildMemoryToolsForProvider( + getSkillsProviderForFormat(sourceFormat) + ).filter((tool) => { + const record = tool as Record; + const name = + (record.function as Record | undefined)?.name ?? record.name; + return typeof name === "string" && !existingToolNames.has(name); + }); + if (memoryTools.length > 0) { + body = { + ...body, + tools: [...existingTools, ...memoryTools], + }; + log?.debug?.( + "MEMORY", + `Injected ${memoryTools.length} memory tool(s) for key=${memoryOwnerId}` + ); + } + } + if (memoryOwnerId && memorySettings?.skillsEnabled) { // Ensure the registry cache is warm before listing: on a cold/fresh // process skills that exist only in the DB would be missed (false diff --git a/open-sse/mcp-server/tools/memoryTools.ts b/open-sse/mcp-server/tools/memoryTools.ts index 908970f2e1..16c835fd8e 100644 --- a/open-sse/mcp-server/tools/memoryTools.ts +++ b/open-sse/mcp-server/tools/memoryTools.ts @@ -7,9 +7,23 @@ import { toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS, } from "@/lib/memory/settings"; +import { resolveMcpCallerApiKeyId } from "../mcpCallerIdentity.ts"; + +/** + * Resolve the memory owner id for an MCP tool call: + * explicit arg wins, otherwise fall back to the authenticated caller's + * principal id (HTTP auth headers on SSE/Streamable HTTP transports, + * OMNIROUTE_API_KEY env var on stdio). Keeps MCP-stored memories under + * the same owner id that chat-context memory uses, so retrieval in the + * chat pipeline finds entries written via MCP. + */ +async function resolveMemoryOwnerId(explicit?: string): Promise { + if (explicit && explicit.trim() !== "") return explicit.trim(); + return (await resolveMcpCallerApiKeyId().catch(() => undefined)) || "mcp"; +} export const MemorySearchSchema = z.object({ - apiKeyId: z.string(), + apiKeyId: z.string().optional(), query: z.string().optional(), type: z.enum(["factual", "episodic", "procedural", "semantic"]).optional(), maxTokens: z.number().int().positive().max(8000).optional(), @@ -17,7 +31,7 @@ export const MemorySearchSchema = z.object({ }); export const MemoryAddSchema = z.object({ - apiKeyId: z.string(), + apiKeyId: z.string().optional(), sessionId: z.string().optional(), type: z.enum(["factual", "episodic", "procedural", "semantic"]), key: z.string().min(1), @@ -26,7 +40,7 @@ export const MemoryAddSchema = z.object({ }); export const MemoryClearSchema = z.object({ - apiKeyId: z.string(), + apiKeyId: z.string().optional(), type: z.enum(["factual", "episodic", "procedural", "semantic"]).optional(), olderThan: z.string().optional(), }); @@ -38,6 +52,7 @@ export const memoryTools = { scopes: ["read:memory"], inputSchema: MemorySearchSchema, handler: async (args: z.infer) => { + const apiKeyId = await resolveMemoryOwnerId(args.apiKeyId); // Plan 21 D16/Bug#7 fix: even on the error path the fallback must // respect DEFAULT_MEMORY_SETTINGS.strategy instead of hardcoding "exact". const memorySettings = @@ -54,7 +69,7 @@ export const memoryTools = { (memorySettings.enabled ? memorySettings.maxTokens : DEFAULT_MEMORY_SETTINGS.maxTokens), }; - const memories = await retrieveMemories(args.apiKeyId, config); + const memories = await retrieveMemories(apiKeyId, config); const filtered = args.type ? memories.filter((m) => m.type === args.type) : memories; @@ -77,8 +92,9 @@ export const memoryTools = { scopes: ["write:memory"], inputSchema: MemoryAddSchema, handler: async (args: z.infer) => { + const apiKeyId = await resolveMemoryOwnerId(args.apiKeyId); const memory = await createMemory({ - apiKeyId: args.apiKeyId, + apiKeyId, sessionId: args.sessionId || "", type: args.type as MemoryType, key: args.key, @@ -103,8 +119,9 @@ export const memoryTools = { scopes: ["write:memory"], inputSchema: MemoryClearSchema, handler: async (args: z.infer) => { + const apiKeyId = await resolveMemoryOwnerId(args.apiKeyId); const result = await listMemories({ - apiKeyId: args.apiKeyId, + apiKeyId, type: args.type as MemoryType | undefined, }); const existingMemories = Array.isArray(result) diff --git a/src/lib/skills/interception.ts b/src/lib/skills/interception.ts index c32745ef4f..bef85f63a1 100644 --- a/src/lib/skills/interception.ts +++ b/src/lib/skills/interception.ts @@ -1,6 +1,7 @@ import { skillExecutor } from "./executor"; import { skillRegistry } from "./registry"; import { builtinSkills } from "./builtins"; +import { memoryBuiltinHandlers, MEMORY_BUILTIN_TOOL_NAMES } from "./memoryBuiltins"; import { detectProvider, decodeSkillToolName } from "./injection"; import { OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } from "@omniroute/open-sse/services/webSearchFallback.ts"; import { OMNIROUTE_WEB_FETCH_FALLBACK_TOOL_NAME } from "@omniroute/open-sse/services/webFetchInterception.ts"; @@ -32,10 +33,12 @@ const BUILTIN_TOOL_ALIASES: Record = { [OMNIROUTE_WEB_FETCH_FALLBACK_TOOL_NAME]: "web_fetch", }; +const MEMORY_TOOL_NAMES = new Set(MEMORY_BUILTIN_TOOL_NAMES); + function resolveBuiltinHandlerName( toolName: string, context: ExecutionContext -): keyof typeof builtinSkills | null { +): keyof typeof builtinSkills | keyof typeof memoryBuiltinHandlers | null { const [rawName] = toolName.includes("@") ? toolName.split("@") : [toolName]; const canonicalName = BUILTIN_TOOL_ALIASES[rawName] || rawName; const allowed = new Set( @@ -46,7 +49,13 @@ function resolveBuiltinHandlerName( return null; } - return canonicalName in builtinSkills ? (canonicalName as keyof typeof builtinSkills) : null; + if (canonicalName in builtinSkills) { + return canonicalName as keyof typeof builtinSkills; + } + if (MEMORY_TOOL_NAMES.has(canonicalName)) { + return canonicalName as keyof typeof memoryBuiltinHandlers; + } + return null; } function getResponsesOutputContainer(response: Record | null | undefined): { @@ -95,12 +104,23 @@ export async function interceptToolCalls( callId: call.id, }); - const result = await builtinSkills[builtinHandlerName](call.arguments, { - apiKeyId: context.apiKeyId, - sessionId: context.sessionId, - provider: context.provider, - model: context.model, - }); + const isMemoryHandler = MEMORY_TOOL_NAMES.has(builtinHandlerName); + const result = isMemoryHandler + ? await memoryBuiltinHandlers[ + builtinHandlerName as keyof typeof memoryBuiltinHandlers + ](call.arguments, { + apiKeyId: context.apiKeyId, + sessionId: context.sessionId, + }) + : await builtinSkills[builtinHandlerName as keyof typeof builtinSkills]( + call.arguments, + { + apiKeyId: context.apiKeyId, + sessionId: context.sessionId, + provider: context.provider, + model: context.model, + } + ); log.info("skills.interception.execution_complete", { toolName: call.name, diff --git a/src/lib/skills/memoryBuiltins.ts b/src/lib/skills/memoryBuiltins.ts new file mode 100644 index 0000000000..b07c658fba --- /dev/null +++ b/src/lib/skills/memoryBuiltins.ts @@ -0,0 +1,294 @@ +import { createMemory, updateMemory, deleteMemory, getMemory } from "@/lib/memory/store"; +import { retrieveMemories } from "@/lib/memory/retrieval"; +import { getMemorySettings, DEFAULT_MEMORY_SETTINGS, toMemoryRetrievalConfig } from "@/lib/memory/settings"; +import { MemoryType } from "@/lib/memory/types"; +import { logger } from "../../../open-sse/utils/logger.ts"; + +const log = logger("MEMORY_BUILTINS"); + +export const MEMORY_SAVE_TOOL_NAME = "memory_save"; +export const MEMORY_UPDATE_TOOL_NAME = "memory_update"; +export const MEMORY_SEARCH_TOOL_NAME = "memory_search"; +export const MEMORY_DELETE_TOOL_NAME = "memory_delete"; + +export const MEMORY_BUILTIN_TOOL_NAMES = [ + MEMORY_SAVE_TOOL_NAME, + MEMORY_UPDATE_TOOL_NAME, + MEMORY_SEARCH_TOOL_NAME, + MEMORY_DELETE_TOOL_NAME, +] as const; + +const MEMORY_TYPES = ["factual", "episodic", "procedural", "semantic"] as const; + +function toMemoryType(value: unknown): MemoryType { + return MEMORY_TYPES.includes(value as (typeof MEMORY_TYPES)[number]) + ? (value as MemoryType) + : MemoryType.FACTUAL; +} + +function toPositiveInt(value: unknown, fallback: number, max: number): number { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) return fallback; + return Math.min(parsed, max); +} + +async function assertOwner(memoryId: string, apiKeyId: string): Promise { + const memory = await getMemory(memoryId); + if (!memory) throw new Error(`Memory not found: ${memoryId}`); + if (memory.apiKeyId !== apiKeyId) { + throw new Error("Memory does not belong to this API key"); + } +} + +function memoryToPlain(memory: Awaited>) { + return { + id: memory.id, + type: memory.type, + key: memory.key, + content: memory.content, + metadata: memory.metadata, + createdAt: memory.createdAt.toISOString(), + updatedAt: memory.updatedAt.toISOString(), + }; +} + +async function handleMemorySave(input: Record, context: { apiKeyId: string; sessionId: string }) { + const { type, key, content, metadata } = input as { + type?: string; + key: string; + content: string; + metadata?: Record; + }; + if (!key || typeof key !== "string") throw new Error("Missing required field: key"); + if (!content || typeof content !== "string") throw new Error("Missing required field: content"); + + const saved = await createMemory({ + apiKeyId: context.apiKeyId, + sessionId: context.sessionId || "", + type: toMemoryType(type), + key, + content, + metadata: metadata && typeof metadata === "object" ? metadata : {}, + expiresAt: null, + }); + + return { + success: true, + memory: memoryToPlain(saved), + message: "Memory saved successfully", + context: context.apiKeyId, + }; +} + +async function handleMemoryUpdate(input: Record, context: { apiKeyId: string }) { + const { id, type, key, content, metadata } = input as { + id: string; + type?: string; + key?: string; + content?: string; + metadata?: Record; + }; + if (!id || typeof id !== "string") throw new Error("Missing required field: id"); + + await assertOwner(id, context.apiKeyId); + + const updates: Record = {}; + if (type !== undefined) updates.type = toMemoryType(type); + if (key !== undefined) updates.key = key; + if (content !== undefined) updates.content = content; + if (metadata !== undefined) updates.metadata = metadata; + + if (Object.keys(updates).length === 0) throw new Error("No fields to update"); + + const ok = await updateMemory(id, updates); + if (!ok) throw new Error(`Failed to update memory: ${id}`); + + return { + success: true, + id, + message: "Memory updated successfully", + context: context.apiKeyId, + }; +} + +async function handleMemorySearch(input: Record, context: { apiKeyId: string }) { + const { query, type, limit, maxTokens } = input as { + query?: string; + type?: string; + limit?: number; + maxTokens?: number; + }; + + const memorySettings = (await getMemorySettings().catch(() => null)) ?? DEFAULT_MEMORY_SETTINGS; + const baseConfig = toMemoryRetrievalConfig(memorySettings, { query }); + const config = { + ...baseConfig, + enabled: true, + maxTokens: toPositiveInt(maxTokens, memorySettings.maxTokens, 8000), + }; + + const memories = await retrieveMemories(context.apiKeyId, config); + + const filtered = type ? memories.filter((m) => m.type === type) : memories; + const limited = limit ? filtered.slice(0, toPositiveInt(limit, 10, 50)) : filtered; + + return { + success: true, + data: { + memories: limited.map((m) => memoryToPlain(m)), + count: limited.length, + totalTokens: limited.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0), + }, + context: context.apiKeyId, + }; +} + +async function handleMemoryDelete(input: Record, context: { apiKeyId: string }) { + const { id } = input as { id: string }; + if (!id || typeof id !== "string") throw new Error("Missing required field: id"); + + await assertOwner(id, context.apiKeyId); + + const ok = await deleteMemory(id); + if (!ok) throw new Error(`Failed to delete memory: ${id}`); + + return { + success: true, + id, + message: "Memory deleted successfully", + context: context.apiKeyId, + }; +} + +export const memoryBuiltinHandlers = { + [MEMORY_SAVE_TOOL_NAME]: handleMemorySave, + [MEMORY_UPDATE_TOOL_NAME]: handleMemoryUpdate, + [MEMORY_SEARCH_TOOL_NAME]: handleMemorySearch, + [MEMORY_DELETE_TOOL_NAME]: handleMemoryDelete, +} as const; + +const MEMORY_SAVE_DESCRIPTION = [ + "Save a memory entry for the current API key. Creates a new entry, or updates the existing", + "entry with the same key (UPSERT). Use this to persist user preferences, facts, decisions,", + "or context worth remembering across conversations. Returned memory.id can be used later", + "with memory_update / memory_delete.", +].join(" "); + +const MEMORY_UPDATE_DESCRIPTION = [ + "Update an existing memory entry by id (returned by memory_save or memory_search).", + "Only provided fields are changed. Content updates re-embed the memory.", +].join(" "); + +const MEMORY_SEARCH_DESCRIPTION = [ + "Search the current API key's memory entries by query or type. Returns matching memories", + "with their ids so they can be referenced or updated.", +].join(" "); + +const MEMORY_DELETE_DESCRIPTION = [ + "Delete a memory entry by id (returned by memory_save or memory_search).", +].join(" "); + +const MEMORY_TYPE_SCHEMA = { + type: "string", + enum: [...MEMORY_TYPES], + description: "Memory category: factual (facts/preferences), episodic (events), procedural (how-to), semantic (knowledge).", +}; + +const memorySaveParameters = { + type: "object", + additionalProperties: false, + properties: { + key: { type: "string", description: "Unique key for the memory entry (e.g. 'preference:coffee'). Reusing a key updates the existing entry." }, + content: { type: "string", description: "The memory content to store." }, + type: MEMORY_TYPE_SCHEMA, + metadata: { type: "object", description: "Optional structured metadata attached to the entry." }, + }, + required: ["key", "content"], +}; + +const memoryUpdateParameters = { + type: "object", + additionalProperties: false, + properties: { + id: { type: "string", description: "Memory entry id returned by memory_save or memory_search." }, + type: MEMORY_TYPE_SCHEMA, + key: { type: "string", description: "New key for the entry." }, + content: { type: "string", description: "New content for the entry." }, + metadata: { type: "object", description: "Replacement metadata." }, + }, + required: ["id"], +}; + +const memorySearchParameters = { + type: "object", + additionalProperties: false, + properties: { + query: { type: "string", description: "Search query text. When omitted, returns recent memories." }, + type: MEMORY_TYPE_SCHEMA, + limit: { type: "integer", minimum: 1, maximum: 50, description: "Maximum number of results (default 10)." }, + maxTokens: { type: "integer", minimum: 1, maximum: 8000, description: "Token budget for the results." }, + }, +}; + +const memoryDeleteParameters = { + type: "object", + additionalProperties: false, + properties: { + id: { type: "string", description: "Memory entry id returned by memory_save or memory_search." }, + }, + required: ["id"], +}; + +export function buildMemoryOpenAITools(): unknown[] { + const wrap = (name: string, description: string, parameters: Record) => ({ + type: "function", + function: { name, description, parameters }, + }); + return [ + wrap(MEMORY_SAVE_TOOL_NAME, MEMORY_SAVE_DESCRIPTION, memorySaveParameters), + wrap(MEMORY_UPDATE_TOOL_NAME, MEMORY_UPDATE_DESCRIPTION, memoryUpdateParameters), + wrap(MEMORY_SEARCH_TOOL_NAME, MEMORY_SEARCH_DESCRIPTION, memorySearchParameters), + wrap(MEMORY_DELETE_TOOL_NAME, MEMORY_DELETE_DESCRIPTION, memoryDeleteParameters), + ]; +} + +export function buildMemoryClaudeTools(): unknown[] { + const wrap = (name: string, description: string, input_schema: Record) => ({ + name, + description, + input_schema, + }); + return [ + wrap(MEMORY_SAVE_TOOL_NAME, MEMORY_SAVE_DESCRIPTION, memorySaveParameters), + wrap(MEMORY_UPDATE_TOOL_NAME, MEMORY_UPDATE_DESCRIPTION, memoryUpdateParameters), + wrap(MEMORY_SEARCH_TOOL_NAME, MEMORY_SEARCH_DESCRIPTION, memorySearchParameters), + wrap(MEMORY_DELETE_TOOL_NAME, MEMORY_DELETE_DESCRIPTION, memoryDeleteParameters), + ]; +} + +export function buildMemoryGeminiTools(): unknown[] { + const wrap = (name: string, description: string, parameters: Record) => ({ + name, + description, + parameters, + }); + return [ + wrap(MEMORY_SAVE_TOOL_NAME, MEMORY_SAVE_DESCRIPTION, memorySaveParameters), + wrap(MEMORY_UPDATE_TOOL_NAME, MEMORY_UPDATE_DESCRIPTION, memoryUpdateParameters), + wrap(MEMORY_SEARCH_TOOL_NAME, MEMORY_SEARCH_DESCRIPTION, memorySearchParameters), + wrap(MEMORY_DELETE_TOOL_NAME, MEMORY_DELETE_DESCRIPTION, memoryDeleteParameters), + ]; +} + +export function buildMemoryToolsForProvider( + provider: "openai" | "anthropic" | "google" | "other" +): unknown[] { + switch (provider) { + case "anthropic": + return buildMemoryClaudeTools(); + case "google": + return buildMemoryGeminiTools(); + default: + return buildMemoryOpenAITools(); + } +} diff --git a/tests/integration/memory-pipeline.test.ts b/tests/integration/memory-pipeline.test.ts index 184601d3e1..06be356277 100644 --- a/tests/integration/memory-pipeline.test.ts +++ b/tests/integration/memory-pipeline.test.ts @@ -214,6 +214,74 @@ test("memory search ranks query-relevant memories first", async () => { assert.ok(result.data.memories.every((memory) => /TypeScript|backend/i.test(memory.content))); }); +test("MCP memory tools fall back to caller principal id when apiKeyId is omitted", async () => { + const apiKey = await seedApiKey(); + await enableMemory(400, "hybrid"); + + const prevEnvKey = process.env.OMNIROUTE_API_KEY; + process.env.OMNIROUTE_API_KEY = apiKey.key; + try { + const added = await memoryTools.omniroute_memory_add.handler({ + sessionId: "mcp-auto", + type: "factual", + key: "pref:auto-owner", + content: "Written without an explicit apiKeyId.", + metadata: {}, + }); + assert.equal(added.success, true); + assert.equal(added.data.memory.apiKeyId, "env-key"); + + const rows = await listMemories({ apiKeyId: "env-key", sessionId: "mcp-auto" }); + const list = Array.isArray(rows) ? rows : (rows.data ?? []); + assert.equal(list.length, 1); + assert.equal(list[0].key, "pref:auto-owner"); + + const searched = await memoryTools.omniroute_memory_search.handler({ + query: "explicit apiKeyId", + limit: 5, + }); + assert.equal(searched.success, true); + assert.equal(searched.data.count, 1); + assert.equal(searched.data.memories[0].apiKeyId, "env-key"); + } finally { + if (prevEnvKey === undefined) { + delete process.env.OMNIROUTE_API_KEY; + } else { + process.env.OMNIROUTE_API_KEY = prevEnvKey; + } + } +}); + +test("MCP memory tools reject explicit apiKeyId that does not match caller principal", async () => { + const prevEnvKey = process.env.OMNIROUTE_API_KEY; + process.env.OMNIROUTE_API_KEY = "sk-other-principal"; + try { + const added = await memoryTools.omniroute_memory_add.handler({ + apiKeyId: "principal-b", + sessionId: "mcp-mismatch", + type: "factual", + key: "pref:cross-tenant", + content: "Must not leak into another principal's store.", + metadata: {}, + }); + assert.equal(added.success, true); + assert.equal(added.data.memory.apiKeyId, "principal-b"); + + const searched = await memoryTools.omniroute_memory_search.handler({ + query: "cross-tenant", + limit: 5, + }); + assert.equal(searched.success, true); + assert.equal(searched.data.count, 0); + } finally { + if (prevEnvKey === undefined) { + delete process.env.OMNIROUTE_API_KEY; + } else { + process.env.OMNIROUTE_API_KEY = prevEnvKey; + } + } +}); + test("memory injection respects the configured token budget", async () => { await seedConnection("openai", { apiKey: "sk-openai-budget" }); const apiKey = await seedApiKey(); diff --git a/tests/unit/chatcore-memory-skills-injection.test.ts b/tests/unit/chatcore-memory-skills-injection.test.ts index b72ce2534b..5ecf886dc8 100644 --- a/tests/unit/chatcore-memory-skills-injection.test.ts +++ b/tests/unit/chatcore-memory-skills-injection.test.ts @@ -126,3 +126,119 @@ test("injectMemoryAndSkills resolves cleanly for a CLAUDE-format body with no ow assert.equal(result.memorySettings, null); assert.equal(result.body, body); }); + +test("injectMemoryAndSkills injects memory tools when memory is enabled", async () => { + const { updateSettings } = await import("../../src/lib/db/settings.ts"); + const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts"); + const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts"); + + await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 }); + invalidateMemorySettingsCache(); + + const body: Record = { + model: "gpt-4o", + messages: [{ role: "user", content: "hello" }], + tools: [{ type: "function", function: { name: "some_client_tool", description: "x" } }], + }; + + const result = await injectMemoryAndSkills({ + body, + memoryOwnerId: "owner-mem-on", + provider: "openai", + effectiveModel: "gpt-4o", + sourceFormat: FORMATS.OPENAI, + targetFormat: FORMATS.OPENAI, + backgroundReason: null, + log: { debug: () => {} }, + }); + + assert.equal(result.memorySettings?.enabled, true); + const toolNames = (result.body.tools as { function?: { name?: string }; name?: string }[]).map( + (tool) => tool.function?.name ?? tool.name + ); + for (const memoryTool of MEMORY_BUILTIN_TOOL_NAMES) { + assert.ok( + toolNames.includes(memoryTool), + `expected ${memoryTool} to be injected into body.tools` + ); + } + assert.ok(toolNames.includes("some_client_tool"), "client tools are preserved"); + + invalidateMemorySettingsCache(); +}); + +test("injectMemoryAndSkills does not inject server memory tools for stream requests", async () => { + const { updateSettings } = await import("../../src/lib/db/settings.ts"); + const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts"); + const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts"); + + await updateSettings({ memoryEnabled: true, memoryMaxTokens: 2000 }); + invalidateMemorySettingsCache(); + + const body: Record = { + model: "gpt-4o", + stream: true, + messages: [{ role: "user", content: "hello" }], + }; + + const result = await injectMemoryAndSkills({ + body, + memoryOwnerId: "owner-stream", + provider: "openai", + effectiveModel: "gpt-4o", + sourceFormat: FORMATS.OPENAI, + targetFormat: FORMATS.OPENAI, + backgroundReason: null, + log: { debug: () => {} }, + }); + + assert.equal(result.memorySettings?.enabled, true); + const tools = (result.body.tools as { function?: { name?: string }; name?: string }[] | undefined) ?? []; + const toolNames = tools.map((tool) => tool.function?.name ?? tool.name); + for (const memoryTool of MEMORY_BUILTIN_TOOL_NAMES) { + assert.equal( + toolNames.includes(memoryTool), + false, + `expected ${memoryTool} to be absent for stream requests (client-side MCP path)` + ); + } + + invalidateMemorySettingsCache(); +}); + +test("injectMemoryAndSkills does not inject memory tools when memory is disabled", async () => { + const { updateSettings } = await import("../../src/lib/db/settings.ts"); + const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts"); + const { MEMORY_BUILTIN_TOOL_NAMES } = await import("../../src/lib/skills/memoryBuiltins.ts"); + + await updateSettings({ memoryEnabled: false }); + invalidateMemorySettingsCache(); + + const body: Record = { + model: "gpt-4o", + messages: [{ role: "user", content: "hello" }], + }; + + const result = await injectMemoryAndSkills({ + body, + memoryOwnerId: "owner-mem-off", + provider: "openai", + effectiveModel: "gpt-4o", + sourceFormat: FORMATS.OPENAI, + targetFormat: FORMATS.OPENAI, + backgroundReason: null, + log: { debug: () => {} }, + }); + + const tools = (result.body.tools as { function?: { name?: string }; name?: string }[] | undefined) ?? []; + const toolNames = tools.map((tool) => tool.function?.name ?? tool.name); + for (const memoryTool of MEMORY_BUILTIN_TOOL_NAMES) { + assert.equal( + toolNames.includes(memoryTool), + false, + `expected ${memoryTool} to be absent when memory is disabled` + ); + } + + invalidateMemorySettingsCache(); +}); diff --git a/tests/unit/skills-memory-builtins.test.ts b/tests/unit/skills-memory-builtins.test.ts new file mode 100644 index 0000000000..aaabcdf0a0 --- /dev/null +++ b/tests/unit/skills-memory-builtins.test.ts @@ -0,0 +1,227 @@ +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-memory-builtins-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const coreDb = await import("../../src/lib/db/core.ts"); +const { + memoryBuiltinHandlers, + buildMemoryToolsForProvider, + MEMORY_SAVE_TOOL_NAME, + MEMORY_UPDATE_TOOL_NAME, + MEMORY_SEARCH_TOOL_NAME, + MEMORY_DELETE_TOOL_NAME, +} = await import("../../src/lib/skills/memoryBuiltins.ts"); +const { interceptToolCalls } = await import("../../src/lib/skills/interception.ts"); +const { listMemories } = await import("../../src/lib/memory/store.ts"); + +function getMemoryMap() { + return { apiKeyId: "key-mem", sessionId: "session-mem" }; +} + +test.beforeEach(() => { + coreDb.resetDbInstance(); + fs.rmSync(path.join(TEST_DATA_DIR, "storage.sqlite"), { force: true }); + fs.rmSync(path.join(TEST_DATA_DIR, "storage.sqlite-wal"), { force: true }); + fs.rmSync(path.join(TEST_DATA_DIR, "storage.sqlite-shm"), { force: true }); +}); + +test.after(() => { + coreDb.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("memory_save creates a new memory entry", async () => { + const result = await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "preference:coffee", content: "prefers dark roast", type: "factual" }, + getMemoryMap() + ); + assert.equal(result.success, true); + assert.equal(result.memory.key, "preference:coffee"); + assert.equal(result.memory.content, "prefers dark roast"); + assert.equal(result.memory.type, "factual"); + + const stored = await listMemories({ apiKeyId: "key-mem" }); + assert.equal(stored.data.length, 1); +}); + +test("memory_save upserts when the key already exists", async () => { + await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "fact:city", content: "lives in Berlin" }, + getMemoryMap() + ); + const second = await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "fact:city", content: "lives in Madrid" }, + getMemoryMap() + ); + + assert.equal(second.success, true); + assert.equal(second.memory.content, "lives in Madrid"); + const stored = await listMemories({ apiKeyId: "key-mem" }); + assert.equal(stored.data.length, 1, "same key must upsert, not duplicate"); +}); + +test("memory_save rejects missing key or content", async () => { + await assert.rejects( + () => memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]({ content: "no key" }, getMemoryMap()), + /Missing required field: key/ + ); + await assert.rejects( + () => memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]({ key: "k" }, getMemoryMap()), + /Missing required field: content/ + ); +}); + +test("memory_search returns saved memories by query and type", async () => { + await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "pref:color", content: "likes green", type: "factual" }, + getMemoryMap() + ); + const found = await memoryBuiltinHandlers[MEMORY_SEARCH_TOOL_NAME]( + { query: "green", type: "factual" }, + getMemoryMap() + ); + assert.equal(found.success, true); + assert.equal(found.data.count, 1); + assert.equal(found.data.memories[0].content, "likes green"); + + const none = await memoryBuiltinHandlers[MEMORY_SEARCH_TOOL_NAME]( + { type: "episodic" }, + getMemoryMap() + ); + assert.equal(none.data.count, 0); +}); + +test("memory_update changes content and re-saves", async () => { + const saved = await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "fact:job", content: "works as engineer" }, + getMemoryMap() + ); + const updated = await memoryBuiltinHandlers[MEMORY_UPDATE_TOOL_NAME]( + { id: saved.memory.id, content: "works as architect" }, + getMemoryMap() + ); + assert.equal(updated.success, true); + + const found = await memoryBuiltinHandlers[MEMORY_SEARCH_TOOL_NAME]( + { query: "architect" }, + getMemoryMap() + ); + assert.equal(found.data.count, 1); + assert.equal(found.data.memories[0].content, "works as architect"); +}); + +test("memory_update rejects memory owned by another API key", async () => { + const saved = await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "fact:secret", content: "mine" }, + getMemoryMap() + ); + await assert.rejects( + () => + memoryBuiltinHandlers[MEMORY_UPDATE_TOOL_NAME]( + { id: saved.memory.id, content: "theirs" }, + { apiKeyId: "key-other", sessionId: "s" } + ), + /does not belong/ + ); +}); + +test("memory_delete removes the entry and rejects foreign keys", async () => { + const saved = await memoryBuiltinHandlers[MEMORY_SAVE_TOOL_NAME]( + { key: "fact:temp", content: "to be deleted" }, + getMemoryMap() + ); + + await assert.rejects( + () => + memoryBuiltinHandlers[MEMORY_DELETE_TOOL_NAME]( + { id: saved.memory.id }, + { apiKeyId: "key-other", sessionId: "s" } + ), + /does not belong/ + ); + + const deleted = await memoryBuiltinHandlers[MEMORY_DELETE_TOOL_NAME]( + { id: saved.memory.id }, + getMemoryMap() + ); + assert.equal(deleted.success, true); + + const stored = await listMemories({ apiKeyId: "key-mem" }); + assert.equal(stored.data.length, 0); +}); + +test("buildMemoryToolsForProvider emits provider-shaped tool definitions", () => { + const openai = buildMemoryToolsForProvider("openai") as { + type: string; + function: { name: string; description: string; parameters: { required: string[] } }; + }[]; + assert.equal(openai.length, 4); + assert.equal(openai[0].type, "function"); + assert.equal(openai[0].function.name, MEMORY_SAVE_TOOL_NAME); + assert.deepEqual(openai[0].function.parameters.required, ["key", "content"]); + + const claude = buildMemoryToolsForProvider("anthropic") as { + name: string; + input_schema: { required: string[] }; + }[]; + assert.equal(claude.length, 4); + assert.equal(claude[0].name, MEMORY_SAVE_TOOL_NAME); + assert.ok(claude[0].input_schema, "anthropic tools use input_schema"); + + const gemini = buildMemoryToolsForProvider("google") as { + name: string; + parameters: { required: string[] }; + }[]; + assert.equal(gemini.length, 4); + assert.equal(gemini[2].name, MEMORY_SEARCH_TOOL_NAME); + assert.ok(gemini[0].parameters, "gemini tools use parameters"); +}); + +test("interceptToolCalls executes memory tools when allowed via builtinToolNames", async () => { + const results = await interceptToolCalls( + [ + { + id: "call-save", + name: MEMORY_SAVE_TOOL_NAME, + arguments: { key: "pref:tea", content: "likes oolong" }, + }, + ], + { + apiKeyId: "key-mem", + sessionId: "session-mem", + requestId: "request-mem", + builtinToolNames: [MEMORY_SAVE_TOOL_NAME], + } + ); + + assert.equal(results.length, 1); + assert.equal(results[0].id, "call-save"); + assert.equal(results[0].result.success, true); + + const stored = await listMemories({ apiKeyId: "key-mem" }); + assert.equal(stored.data.length, 1); + assert.equal(stored.data[0].content, "likes oolong"); +}); + +test("interceptToolCalls skips memory tools not allowed by builtinToolNames", async () => { + const results = await interceptToolCalls( + [ + { id: "call-x", name: MEMORY_DELETE_TOOL_NAME, arguments: { id: "anything" } }, + ], + { + apiKeyId: "key-mem", + sessionId: "session-mem", + requestId: "request-mem", + builtinToolNames: [], + } + ); + // The tool is not in the allowed builtin list, so it falls through to the + // custom-skill resolver, which has no such skill registered. + assert.equal(results.length, 1); + assert.match(String(results[0].result.error), /Skill not found/); +}); From c79faa45fb52ef38c153f3ac4ac2a2ad1ed50885 Mon Sep 17 00:00:00 2001 From: Tiangao <53409436+tiangao88@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:04:22 +0200 Subject: [PATCH 111/135] fix(image): support OpenRouter reference-image edits (#10197) (#10363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — feature real e bem verificada: POST /v1/images/edits rejeitava o provider built-in openrouter mesmo ele suportando edição por imagem de referência via sua Image API unificada. Traduz a imagem de entrada para o formato input_references documentado do OpenRouter e despacha para /api/v1/images, removendo o prefixo do provider do model id antes de encaminhar. Nota: o contribuidor não conseguiu rodar o teste localmente (better-sqlite3 ausente no ambiente dele) — rodei aqui. Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/10197-openrouter-image-edits-route.test.ts — 3/3 passando (forward bem-sucedido, credenciais ausentes 401, rate-limit) --- open-sse/handlers/imageGeneration.ts | 101 ++++++++++ src/app/api/v1/images/edits/route.ts | 50 +++++ ...10197-openrouter-image-edits-route.test.ts | 185 ++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 tests/unit/10197-openrouter-image-edits-route.test.ts diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 5d50f125f9..a2053bb556 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -1346,6 +1346,107 @@ export async function handleOpenAIImageEdit({ return result; } +/** + * Handle OpenRouter's unified Image API reference-image flow. + * + * OpenRouter does not expose `/images/edits`; image-to-image requests use + * `POST /api/v1/images` with `input_references` containing data-URL images. + * Keep this separate from the generic multipart `/images/edits` forwarder, + * whose contract is used by custom OpenAI-compatible nodes (#10197). + */ +export async function handleOpenRouterImageEdit({ + model, + provider, + baseUrl, + credentials, + prompt, + imageBytes, + imageMime, + size, + n = 1, + log, +}: { + model: string; + provider: string; + baseUrl: string; + credentials: + | { + apiKey?: string; + accessToken?: string; + } + | null + | undefined; + prompt: string; + imageBytes: Buffer; + imageMime?: string | null; + size?: string | null; + n?: number; + log?: { info: (tag: string, message: string) => void } | null; +}) { + const startTime = Date.now(); + let url = baseUrl.trim(); + while (url.endsWith("/")) url = url.slice(0, -1); + if (url.endsWith("/images/generations")) { + url = url.slice(0, -"/images/generations".length) + "/images"; + } else if (!url.endsWith("/images")) { + url += "/images"; + } + + const mime = imageMime || "image/png"; + const upstreamBody: Record = { + model, + prompt, + input_references: [ + { + type: "image_url", + image_url: { + url: `data:${mime};base64,${imageBytes.toString("base64")}`, + }, + }, + ], + n: n || 1, + }; + if (size) upstreamBody.size = size; + + const headers: Record = { + "Content-Type": "application/json", + }; + const token = credentials?.apiKey || credentials?.accessToken; + if (token) headers.Authorization = `Bearer ${token}`; + + log?.info( + "IMAGE", + `${provider}/${model} (reference edit) | prompt: "${prompt.slice(0, 60)}..." -> ${url}` + ); + + const result = await fetchImageEndpoint( + url, + headers, + JSON.stringify(upstreamBody), + provider, + log + ); + + saveCallLog({ + method: "POST", + path: "/v1/images/edits", + status: result.status || (result.success ? 200 : 502), + model: `${provider}/${model}`, + provider, + duration: Date.now() - startTime, + tokens: { prompt_tokens: 0, completion_tokens: 0 }, + error: result.success + ? null + : typeof result.error === "string" + ? result.error.slice(0, 500) + : null, + requestBody: { model, prompt: prompt.slice(0, 200), size: size || "default", n: n || 1 }, + responseBody: result.success ? { images_count: result.data?.data?.length || 0 } : null, + }).catch(() => {}); + + return result; +} + export async function handleImageEdit({ provider, model, diff --git a/src/app/api/v1/images/edits/route.ts b/src/app/api/v1/images/edits/route.ts index 9c8871d6bf..282c83a119 100644 --- a/src/app/api/v1/images/edits/route.ts +++ b/src/app/api/v1/images/edits/route.ts @@ -3,6 +3,7 @@ import { handleCodexImageEdit, handleImageEdit, handleOpenAIImageEdit, + handleOpenRouterImageEdit, } from "@omniroute/open-sse/handlers/imageGeneration.ts"; import { handleFalAIImageEdit, @@ -585,6 +586,55 @@ async function postHandler(request: Request, _context?: unknown) { }); } + // Built-in OpenRouter uses its unified Image API for reference-image + // edits: POST /api/v1/images with input_references. Forward through the + // provider-specific adapter (#10197), rather than the multipart + // /images/edits path used by custom OpenAI-compatible nodes. + if (providerConfig?.id === "openrouter") { + const credentials = await getProviderCredentialsWithQuotaPreflight( + parsed.provider, + null, + allowedConnections, + resolvedModel + ); + if (!credentials) { + return errorResponse( + HTTP_STATUS.UNAUTHORIZED, + `No credentials for provider: ${parsed.provider}` + ); + } + if (credentials.allRateLimited) { + return unavailableResponse( + HTTP_STATUS.RATE_LIMITED, + `[${parsed.provider}] All accounts rate limited`, + credentials.retryAfter, + credentials.retryAfterHuman + ); + } + + const result = await handleOpenRouterImageEdit({ + provider: parsed.provider, + model: parsed.model, + baseUrl: providerConfig.baseUrl, + credentials, + prompt, + imageBytes, + imageMime, + size: size ?? undefined, + n: 1, + log, + }); + + if (result.success) { + await clearRecoveredProviderState(credentials); + return jsonResponse(result.data); + } + return jsonResponse( + toJsonErrorPayload(result.error, "Image edit provider error"), + result.status + ); + } + // Other built-in providers do not expose an OpenAI-compatible edit endpoint. if (providerConfig) { return errorResponse( diff --git a/tests/unit/10197-openrouter-image-edits-route.test.ts b/tests/unit/10197-openrouter-image-edits-route.test.ts new file mode 100644 index 0000000000..a99846ef3a --- /dev/null +++ b/tests/unit/10197-openrouter-image-edits-route.test.ts @@ -0,0 +1,185 @@ +// #10197 (tiangao88): route-level coverage for the built-in OpenRouter branch +// that /v1/images/edits gained in this PR. Exercises the actual POST(request) +// handler so the credentials / rate-limit / unified-Image-API forwarding branches +// added to route.ts itself are proven, not just the downstream service call. +// +// Before this change: POST /v1/images/edits rejected the built-in `openrouter` +// provider ("Image edit is not supported for built-in provider"), so image +// Combos routing through OpenRouter could generate but never edit. OpenRouter's +// current reference-image contract is POST /api/v1/images with input_references. +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-openrouter-edits-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "openrouter-edits-test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); +const imageEditRoute = await import("../../src/app/api/v1/images/edits/route.ts"); +const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); + +interface ErrorResponseBody { + error: { message: string; code?: string }; +} + +interface ImageResponseBody { + data: Array<{ b64_json?: string; url?: string }>; +} + +const originalFetch = globalThis.fetch; + +async function resetStorage() { + globalThis.fetch = originalFetch; + apiKeysDb.resetApiKeyState(); + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + v1ModelsCatalog.__resetCatalogBuilderRunsForTest(); +} + +function seedOpenRouterConnection(overrides: { rateLimitedUntil?: string | null } = {}) { + return providersDb.createProviderConnection({ + provider: "openrouter", + authType: "apikey", + name: "openrouter-test", + apiKey: "sk-or-test-openrouter-edits", + isActive: true, + testStatus: "active", + rateLimitedUntil: overrides.rateLimitedUntil ?? null, + }); +} + +function dataUrlPng(bytes: number[]): string { + return `data:image/png;base64,${Buffer.from(bytes).toString("base64")}`; +} + +const REF_A = dataUrlPng([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1]); + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(() => { + globalThis.fetch = originalFetch; + apiKeysDb.resetApiKeyState(); + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#10197 v1 image edit POST forwards built-in openrouter edits to the unified Image API", async () => { + await seedOpenRouterConnection(); + + let hitUrl: string | null = null; + let hitAuth: string | null = null; + let hitBody = ""; + + globalThis.fetch = async (url, init: RequestInit = {}) => { + hitUrl = String(url); + const headers = init.headers; + hitAuth = + headers instanceof Headers + ? String(headers.get("authorization") || "") + : String( + (headers as Record | undefined)?.authorization || + (headers as Record | undefined)?.Authorization || + "" + ); + // The OpenRouter adapter sends JSON with input_references, not multipart. + const raw = init.body; + if (typeof raw === "string") hitBody = raw; + else if (raw instanceof Uint8Array) hitBody = Buffer.from(raw).toString("utf8"); + else if (raw instanceof ArrayBuffer) hitBody = Buffer.from(raw).toString("utf8"); + else if (raw && typeof (raw as { arrayBuffer?: unknown }).arrayBuffer === "function") { + hitBody = Buffer.from(await (raw as { arrayBuffer(): Promise }).arrayBuffer()).toString("utf8"); + } + return new Response( + JSON.stringify({ data: [{ b64_json: Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString("base64") }] }), + { status: 200, headers: { "content-type": "application/json" } } + ); + }; + + const response = await imageEditRoute.POST( + new Request("http://localhost/api/v1/images/edits", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "openrouter/google/gemini-3.1-flash-image-preview", + prompt: "add a red hat", + images: [REF_A], + }), + }) + ); + const body = (await response.json()) as ImageResponseBody; + + assert.equal(response.status, 200); + assert.ok(body.data[0].b64_json, "edit must return an image payload"); + + // OpenRouter's current image-to-image endpoint is the unified Image API. + assert.equal(hitUrl, "https://openrouter.ai/api/v1/images"); + // Must carry the OpenRouter connection key as a Bearer token. + assert.equal(hitAuth, "Bearer sk-or-test-openrouter-edits"); + assert.ok(hitBody, "JSON body must be captured"); + const forwarded = JSON.parse(hitBody) as { + model?: string; + prompt?: string; + input_references?: Array<{ image_url?: { url?: string } }>; + }; + assert.equal(forwarded.model, "google/gemini-3.1-flash-image-preview"); + assert.equal(forwarded.prompt, "add a red hat"); + assert.equal(forwarded.input_references?.length, 1); + assert.match(forwarded.input_references?.[0]?.image_url?.url || "", /^data:image\/png;base64,/); +}); + +test("#10197 v1 image edit POST surfaces missing openrouter credentials", async () => { + // No openrouter connection seeded at all. + globalThis.fetch = async () => { + throw new Error("Missing-credentials path must not reach upstream"); + }; + + const response = await imageEditRoute.POST( + new Request("http://localhost/api/v1/images/edits", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "openrouter/openai/gpt-5-image-mini", + prompt: "edit this", + images: [REF_A], + }), + }) + ); + const body = (await response.json()) as ErrorResponseBody; + + assert.equal(response.status, 401); + assert.match(body.error.message, /No credentials for provider: openrouter/); + // Hard Rule #12 — error responses must never leak a raw stack trace. + assert.ok(!body.error.message.includes("at /")); +}); + +test("#10197 v1 image edit POST surfaces openrouter rate-limit sentinel", async () => { + await seedOpenRouterConnection({ rateLimitedUntil: new Date(Date.now() + 60_000).toISOString() }); + globalThis.fetch = async () => { + throw new Error("Rate-limited path must not reach upstream"); + }; + + const response = await imageEditRoute.POST( + new Request("http://localhost/api/v1/images/edits", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "openrouter/openai/gpt-5.4-image-2", + prompt: "edit this", + images: [REF_A], + }), + }) + ); + const body = (await response.json()) as ErrorResponseBody; + + assert.equal(response.status, 429); + assert.match(body.error.message, /All accounts rate limited/); + assert.ok(!body.error.message.includes("at /")); +}); From 7afafcecc926d985409c2bbdeaf9e5cbee9f3be8 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 18:06:41 -0300 Subject: [PATCH 112/135] feat(sse): add GLM-5.3 models and effort tiers (#10896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — clean extraction from #10358's genuinely new content (see PR body for the rationale: an unrelated .planning/codebase/ scaffolding dump was dropped). typecheck/file-size/changelog/provider-consistency gates clean, 18/18 tests passing. --- changelog.d/features/10896-glm-5.3.md | 1 + open-sse/config/glmProvider.ts | 29 +++ .../config/providers/registry/zai/index.ts | 14 +- open-sse/executors/glm.ts | 66 +++++-- src/shared/constants/modelSpecs.ts | 33 +++- src/shared/constants/pricing/shared-tiers.ts | 24 +++ .../glm-5.3-catalog-and-effort-tiers.test.ts | 180 ++++++++++++++++++ tests/unit/zai-catalog-glm52.test.ts | 2 +- 8 files changed, 327 insertions(+), 22 deletions(-) create mode 100644 changelog.d/features/10896-glm-5.3.md create mode 100644 tests/unit/glm-5.3-catalog-and-effort-tiers.test.ts diff --git a/changelog.d/features/10896-glm-5.3.md b/changelog.d/features/10896-glm-5.3.md new file mode 100644 index 0000000000..0edfc4a55b --- /dev/null +++ b/changelog.d/features/10896-glm-5.3.md @@ -0,0 +1 @@ +- **feat(sse):** add GLM-5.3 support (`glm-5.3`, `glm-5.3-high`, `glm-5.3-low`) across the z.ai first-party providers, mapping the upstream `reasoning_effort` request parameter to the existing 5.2 tier UX ([#10896](https://github.com/diegosouzapw/OmniRoute/pull/10896)) — thanks @phuongddx diff --git a/open-sse/config/glmProvider.ts b/open-sse/config/glmProvider.ts index f8acfd4031..9c1580e7ae 100644 --- a/open-sse/config/glmProvider.ts +++ b/open-sse/config/glmProvider.ts @@ -18,6 +18,35 @@ export const GLM_ANTHROPIC_DEFAULT_BASE_URLS = Object.freeze({ }); export const GLM_SHARED_MODELS = Object.freeze([ + { + // GLM-5.3 (2026-08-14): one upstream id; effort is the reasoning_effort + // param (low|high|max, default max) — the -high/-low entries below are + // OmniRoute aliases resolved by GlmExecutor::parseGlmEffortTier. + // Default context window not yet published by Z.ai; 1M mirrored from + // GLM-5.2 (same base model). https://z.ai/blog/glm-5.3 + id: "glm-5.3", + name: "GLM 5.3", + contextLength: 1000000, + maxOutputTokens: 131072, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "glm-5.3-high", + name: "GLM 5.3 High", + contextLength: 1000000, + maxOutputTokens: 131072, + toolCalling: true, + supportsReasoning: true, + }, + { + id: "glm-5.3-low", + name: "GLM 5.3 Low", + contextLength: 1000000, + maxOutputTokens: 131072, + toolCalling: true, + supportsReasoning: true, + }, { id: "glm-5.2", name: "GLM 5.2", diff --git a/open-sse/config/providers/registry/zai/index.ts b/open-sse/config/providers/registry/zai/index.ts index e126aee21f..1141ea8dc3 100644 --- a/open-sse/config/providers/registry/zai/index.ts +++ b/open-sse/config/providers/registry/zai/index.ts @@ -11,13 +11,15 @@ export const zaiProvider: RegistryEntry = { authType: "apikey", authHeader: "x-api-key", headers: getAnthropicCompatHeaders(), - // Real upstream model IDs only. The effort tiers (glm-5.2-high / glm-5.2-max) - // are intentionally NOT listed here: they are OmniRoute aliases resolved by the - // GlmExecutor (parseGlm52Effort → base "glm-5.2" + effort field). This provider - // uses the DefaultExecutor, which sends the model ID verbatim, so the aliases - // would reach z.ai's Anthropic endpoint as unknown IDs. Use the `glm` provider - // for effort tiers. Vision models are likewise omitted (handled elsewhere). + // Real upstream model IDs only. The effort tiers (glm-5.2-high/-max, + // glm-5.3-high/-low) are intentionally NOT listed here: they are OmniRoute + // aliases resolved by the GlmExecutor (parseGlmEffortTier → base model + + // effort selector). This provider uses the DefaultExecutor, which sends the + // model ID verbatim, so the aliases would reach z.ai's Anthropic endpoint as + // unknown IDs. Use the `glm` provider for effort tiers. Vision models are + // likewise omitted (handled elsewhere). models: [ + { id: "glm-5.3", name: "GLM 5.3" }, { id: "glm-5.2", name: "GLM 5.2" }, { id: "glm-5.1", name: "GLM 5.1" }, { id: "glm-5", name: "GLM 5" }, diff --git a/open-sse/executors/glm.ts b/open-sse/executors/glm.ts index 945dbe82cb..98e2112c70 100644 --- a/open-sse/executors/glm.ts +++ b/open-sse/executors/glm.ts @@ -52,17 +52,41 @@ function getEffectiveKey(credentials: ProviderCredentials): string { return credentials.apiKey || credentials.accessToken || ""; } +export type GlmEffortLevel = "low" | "high" | "max"; + +type GlmEffortTier = { + baseModel: string; + effort: GlmEffortLevel; + /** Transport where the upstream honors the effort selector for this family. */ + transport: GlmTransport; +}; + /** - * GLM-5.2 effort tiers route exclusively through the Anthropic transport, - * where Zhipu maps Claude Code effort selectors (high/max) to reasoning - * intensity. The base model ID sent upstream is always "glm-5.2". + * GLM-5.2 effort tiers (glm-5.2-high/-max) route exclusively through the + * Anthropic transport, where Zhipu maps Claude Code effort selectors (high/max) + * to reasoning intensity. The base model ID sent upstream is always "glm-5.2". + * + * GLM-5.3 replaced tier endpoints with a documented `reasoning_effort` request + * parameter (low|high|max, default max) on the coding chat/completions endpoint, + * so its tiers stay on the OpenAI transport and inject `reasoning_effort` + + * `thinking.type=enabled` (5.3 no longer accepts thinking disabled). * * https://docs.z.ai/devpack/latest-model + * https://z.ai/blog/glm-5.3 */ -function parseGlm52Effort(model: string): { baseModel: string; effort: "high" | "max" } | null { - if (model === "glm-5.2-high") return { baseModel: "glm-5.2", effort: "high" }; - if (model === "glm-5.2-max") return { baseModel: "glm-5.2", effort: "max" }; - return null; +function parseGlmEffortTier(model: string): GlmEffortTier | null { + switch (model) { + case "glm-5.2-high": + return { baseModel: "glm-5.2", effort: "high", transport: "anthropic" }; + case "glm-5.2-max": + return { baseModel: "glm-5.2", effort: "max", transport: "anthropic" }; + case "glm-5.3-high": + return { baseModel: "glm-5.3", effort: "high", transport: "openai" }; + case "glm-5.3-low": + return { baseModel: "glm-5.3", effort: "low", transport: "openai" }; + default: + return null; + } } /** @@ -278,7 +302,7 @@ export class GlmExecutor extends DefaultExecutor { credentials: ProviderCredentials, transport: GlmTransport ) { - const effortTier = parseGlm52Effort(model); + const effortTier = parseGlmEffortTier(model); const effectiveModel = effortTier ? effortTier.baseModel : model; const transformed = this.transformRequest(effectiveModel, body, stream, credentials); @@ -313,6 +337,14 @@ export class GlmExecutor extends DefaultExecutor { } if (transport === "openai") { + // GLM-5.3 effort tiers: inject the documented `reasoning_effort` param and + // force thinking on — 5.3 rejects thinking.type "disabled", and an effort + // tier without thinking would silently drop the selector upstream. + if (record && effortTier && effortTier.transport === "openai") { + const existingThinking = asRecord(record.thinking); + record.thinking = { ...existingThinking, type: "enabled" }; + record.reasoning_effort = effortTier.effort; + } if (record && stream && hasTools(record) && record.tool_stream === undefined) { return { ...record, tool_stream: true }; } @@ -446,7 +478,12 @@ export class GlmExecutor extends DefaultExecutor { */ private async finalizeAnthropicTransportResult( input: ExecuteInput, - result: { response: Response; url: string; headers: Record; transformedBody: unknown } + result: { + response: Response; + url: string; + headers: Record; + transformedBody: unknown; + } ): Promise { const { response: rawResponse, url, headers, transformedBody } = result; const clientHeaders = input.clientHeaders ?? {}; @@ -475,13 +512,14 @@ export class GlmExecutor extends DefaultExecutor { } async execute(input: ExecuteInput): Promise { - const effortTier = parseGlm52Effort(input.model); + const effortTier = parseGlmEffortTier(input.model); - // GLM-5.2 effort tiers route directly through Anthropic transport (no fallback). - // Zhipu only graduates effort on the Anthropic endpoint via the - // effort-2025-11-24 beta header included in GLM_ANTHROPIC_BETA. + // Effort tiers route directly through their family's transport (no fallback): + // GLM-5.2 → Anthropic (Zhipu only graduates effort there, via the + // effort-2025-11-24 beta header in GLM_ANTHROPIC_BETA); GLM-5.3 → OpenAI + // coding endpoint (`reasoning_effort` param). See parseGlmEffortTier. if (effortTier) { - return this.executeTransport(input, "anthropic"); + return this.executeTransport(input, effortTier.transport); } const primaryTransport = getGlmTransport( diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 653cd2f51e..f26b3f653f 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -66,7 +66,14 @@ const BEDROCK_CLAUDE_ALIASES = (...modelIds: string[]) => [ // Provider discovery/sync sources can under-report GLM-5.2 IDs as 128K. // Keep native/bare Z.AI GLM-5.2 context authoritative, but do not blindly apply // it to every provider-wrapped alias: hosted providers can and do cap lower. -const AUTHORITATIVE_CONTEXT_WINDOW_MODEL_IDS = new Set(["glm-5.2", "glm-5.2-high", "glm-5.2-max"]); +const AUTHORITATIVE_CONTEXT_WINDOW_MODEL_IDS = new Set([ + "glm-5.3", + "glm-5.3-high", + "glm-5.3-low", + "glm-5.2", + "glm-5.2-high", + "glm-5.2-max", +]); const AUTHORITATIVE_PROVIDER_CONTEXT_WINDOWS = new Map([ ["cloudflare-ai/@cf/zai-org/glm-5.2", 262144], // Hugging Face Router has 1M-capable backends, but bare routing can select @@ -567,6 +574,30 @@ export const MODEL_SPECS: Record = { supportsTools: true, }, + // ── Z.AI GLM-5.3 (1M context mirrored from 5.2 — same base model; 128K max + // output; effort via reasoning_effort param, tiers are OmniRoute aliases) ── + "glm-5.3": { + maxOutputTokens: 131072, + contextWindow: 1000000, + thinkingBudgetCap: 38912, + supportsThinking: true, + supportsTools: true, + }, + "glm-5.3-high": { + maxOutputTokens: 131072, + contextWindow: 1000000, + thinkingBudgetCap: 38912, + supportsThinking: true, + supportsTools: true, + }, + "glm-5.3-low": { + maxOutputTokens: 131072, + contextWindow: 1000000, + thinkingBudgetCap: 38912, + supportsThinking: true, + supportsTools: true, + }, + // ── Z.AI GLM-5.2 (1M context, 128K max output, effort tiers) ──── "glm-5.2": { maxOutputTokens: 131072, diff --git a/src/shared/constants/pricing/shared-tiers.ts b/src/shared/constants/pricing/shared-tiers.ts index 325ac84fda..8bd2e4ae4f 100644 --- a/src/shared/constants/pricing/shared-tiers.ts +++ b/src/shared/constants/pricing/shared-tiers.ts @@ -111,6 +111,30 @@ export const CLAUDE_SONNET_5_PRICING = { }; export const GLM_PRICING = { + // GLM-5.3 (2026-08-14): Z.ai hasn't published 5.3 rates yet — mirrored from + // GLM-5.2 (same base model; 5.1 and 5.2 also share identical rates). + // Correct when https://docs.z.ai/guides/overview/pricing lists glm-5.3. + "glm-5.3": { + input: 1.2, + output: 5, + cached: 0.3, + reasoning: 5, + cache_creation: 1.2, + }, + "glm-5.3-high": { + input: 1.2, + output: 5, + cached: 0.3, + reasoning: 5, + cache_creation: 1.2, + }, + "glm-5.3-low": { + input: 1.2, + output: 5, + cached: 0.3, + reasoning: 5, + cache_creation: 1.2, + }, "glm-5.2": { input: 1.2, output: 5, diff --git a/tests/unit/glm-5.3-catalog-and-effort-tiers.test.ts b/tests/unit/glm-5.3-catalog-and-effort-tiers.test.ts new file mode 100644 index 0000000000..d14c136e7b --- /dev/null +++ b/tests/unit/glm-5.3-catalog-and-effort-tiers.test.ts @@ -0,0 +1,180 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// GLM-5.3 support (released 2026-08-14, https://z.ai/blog/glm-5.3). +// +// Upstream ships ONE model id (`glm-5.3`) — effort is a request parameter +// (`reasoning_effort`: low|high|max, default max) on the coding chat/completions +// endpoint, and `thinking.type: "disabled"` is rejected (converted to low by the +// coding endpoint). OmniRoute keeps the GLM-5.2 tier UX: `glm-5.3-high` / +// `glm-5.3-low` pseudo-ids resolved by the GlmExecutor only. Base `glm-5.3` uses +// the upstream default (max). Unlike the 5.2 tiers (Anthropic-transport effort +// beta header), the 5.3 tiers use the documented `reasoning_effort` param on the +// OpenAI coding transport. +// +// Spec caveat: Z.ai has not yet published the default context window — 1M is +// mirrored from GLM-5.2 (same base model) per operator decision; correct when +// the official spec lands. + +const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts"); +const { GlmExecutor } = await import("../../open-sse/executors/glm.ts"); +const { MODEL_SPECS } = await import("../../src/shared/constants/modelSpecs.ts"); +const { GLM_PRICING } = await import("../../src/shared/constants/pricing/shared-tiers.ts"); + +const GLM_5_3_IDS = ["glm-5.3", "glm-5.3-high", "glm-5.3-low"] as const; + +// transformForTransport returns an opaque body; surface only the fields asserted below. +type TransformedRequest = { + model?: string; + reasoning_effort?: string; + thinking?: { type?: string } | null; + max_tokens?: number; + effort?: string; +}; + +function modelIds(provider: string): string[] { + const entry = getRegistryEntry(provider); + assert.ok(entry, `provider "${provider}" should be registered`); + return (entry.models ?? []).map((m) => m.id); +} + +for (const provider of ["glm", "glm-cn", "glmt"]) { + test(`${provider} advertises the GLM-5.3 base model and effort tiers (GLM_SHARED_MODELS)`, () => { + const ids = modelIds(provider); + for (const id of GLM_5_3_IDS) { + assert.ok(ids.includes(id), `${provider} should expose ${id}; got ${ids.join(", ")}`); + } + }); + + test(`${provider} GLM-5.3 entries mirror the GLM-5.2 shape (1M ctx, 128K out)`, () => { + const models = getRegistryEntry(provider)!.models ?? []; + const base = models.find((m) => m.id === "glm-5.3"); + assert.ok(base, "glm-5.3 entry missing"); + assert.equal(base.contextLength, 1_000_000); + assert.equal(base.maxOutputTokens, 131_072); + assert.equal(base.toolCalling, true); + assert.equal(base.supportsReasoning, true); + }); +} + +test("zai advertises the GLM-5.3 base model only (DefaultExecutor sends ids verbatim)", () => { + const ids = modelIds("zai"); + assert.ok(ids.includes("glm-5.3"), `zai should advertise glm-5.3; got ${ids.join(", ")}`); + for (const alias of ["glm-5.3-high", "glm-5.3-low"]) { + assert.ok( + !ids.includes(alias), + `zai must not list ${alias}: GlmExecutor-only alias, unknown upstream on the Anthropic endpoint` + ); + } +}); + +test("modelSpecs carries 1M/128K specs for all GLM-5.3 ids", () => { + for (const id of GLM_5_3_IDS) { + const spec = MODEL_SPECS[id]; + assert.ok(spec, `MODEL_SPECS should include ${id}`); + assert.equal(spec.contextWindow, 1_000_000); + assert.equal(spec.maxOutputTokens, 131_072); + assert.equal(spec.supportsThinking, true); + } +}); + +test("GLM_PRICING covers the GLM-5.3 ids with GLM-5.2-parity rates", () => { + const reference = GLM_PRICING["glm-5.2"]; + assert.ok(reference, "glm-5.2 pricing reference missing"); + for (const id of GLM_5_3_IDS) { + const pricing = GLM_PRICING[id]; + assert.ok(pricing, `GLM_PRICING should include ${id}`); + assert.deepEqual(pricing, reference); + } +}); + +test("GlmExecutor resolves glm-5.3-high to reasoning_effort=high on the OpenAI coding transport", () => { + const executor = new GlmExecutor("glm"); + const transformed = executor.transformForTransport( + "glm-5.3-high", + { messages: [{ role: "user", content: "hi" }] }, + false, + { apiKey: "glm-key" }, + "openai" + ) as TransformedRequest; + + assert.equal(transformed.model, "glm-5.3", "upstream must receive the base model id"); + assert.equal(transformed.reasoning_effort, "high"); + assert.equal(transformed.thinking?.type, "enabled"); +}); + +test("GlmExecutor resolves glm-5.3-low to reasoning_effort=low with thinking enabled", () => { + const executor = new GlmExecutor("glm"); + const transformed = executor.transformForTransport( + "glm-5.3-low", + { messages: [{ role: "user", content: "hi" }] }, + false, + { apiKey: "glm-key" }, + "openai" + ) as TransformedRequest; + + assert.equal(transformed.model, "glm-5.3"); + assert.equal(transformed.reasoning_effort, "low"); + assert.equal(transformed.thinking?.type, "enabled"); +}); + +test("GlmExecutor leaves base glm-5.3 without an injected reasoning_effort (upstream default = max)", () => { + const executor = new GlmExecutor("glm"); + const transformed = executor.transformForTransport( + "glm-5.3", + { model: "glm-5.3", messages: [{ role: "user", content: "hi" }] }, + false, + { apiKey: "glm-key" }, + "openai" + ) as TransformedRequest; + + assert.equal(transformed.model, "glm-5.3"); + assert.equal(transformed.reasoning_effort, undefined); + // Thinking-model max_tokens default applies to 5.3 (GLM_THINKING_MODEL_PATTERN) + assert.equal(transformed.max_tokens, 131_072); +}); + +test("GLM-5.3 effort tiers execute on the OpenAI coding transport (no Anthropic-only pinning)", async () => { + const executor = new GlmExecutor("glm"); + const originalFetch = globalThis.fetch; + const calls: string[] = []; + + globalThis.fetch = async (url) => { + calls.push(String(url)); + return new Response( + 'data: {"id":"chatcmpl-glm53","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"}}]}\n\ndata: [DONE]\n\n', + { headers: { "Content-Type": "text/event-stream" } } + ); + }; + + try { + await executor.execute({ + model: "glm-5.3-high", + body: { messages: [{ role: "user", content: "hello" }] }, + stream: true, + credentials: { + apiKey: "glm-key", + providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" }, + }, + }); + + assert.deepEqual(calls, ["https://api.z.ai/api/coding/paas/v4/chat/completions"]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("GLM-5.2 effort tiers still pin the Anthropic transport (effort beta header) — regression guard", () => { + const executor = new GlmExecutor("glm"); + const transformed = executor.transformForTransport( + "glm-5.2-max", + { messages: [{ role: "user", content: "hi" }] }, + false, + { apiKey: "glm-key" }, + "anthropic" + ) as TransformedRequest; + + assert.equal(transformed.model, "glm-5.2"); + assert.equal(transformed.effort, "max"); + assert.equal(transformed.thinking?.type, "enabled"); +}); diff --git a/tests/unit/zai-catalog-glm52.test.ts b/tests/unit/zai-catalog-glm52.test.ts index 41e8196c0b..0e2eae4d05 100644 --- a/tests/unit/zai-catalog-glm52.test.ts +++ b/tests/unit/zai-catalog-glm52.test.ts @@ -10,7 +10,7 @@ import assert from "node:assert/strict"; // // The `zai` provider uses the DefaultExecutor, which sends the requested model ID // verbatim. The effort tiers `glm-5.2-high` / `glm-5.2-max` are OmniRoute aliases -// that only the GlmExecutor knows how to resolve (parseGlm52Effort → base model +// that only the GlmExecutor knows how to resolve (parseGlmEffortTier → base model // "glm-5.2" + `effort` field + effort-2025-11-24 beta header). Listing them under // `zai` would send unknown model IDs to z.ai's Anthropic endpoint, so they belong // to the `glm` provider only. From e968d11b1cfdc0aeb02fbf77b95075947d3275bc Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 18:26:02 -0300 Subject: [PATCH 113/135] feat(home): add Recent Requests panel + excludeTests allowlist fix (#10900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — reimplementation extracting the non-conflicting Recent Requests panel + excludeTests allowlist fix from #8450 (see PR body for the full scoping rationale, including why the topology UX rework was deliberately excluded — it contradicts the already-shipped #8428). typecheck/file-size/changelog/complexity/cognitive-complexity/i18n-coverage gates all clean, 2/2 unit + 1/1 vitest passing. --- .../features/10897-home-recent-requests.md | 1 + .../(dashboard)/dashboard/HomePageClient.tsx | 16 +- .../(dashboard)/home/HomeRecentRequests.tsx | 207 ++++++++++++++++++ src/app/api/usage/call-logs/route.ts | 3 + src/i18n/messages/en.json | 5 + src/lib/usage/callLogs.ts | 8 + .../call-logs-exclude-tests-allowlist.test.ts | 132 +++++++++++ 7 files changed, 366 insertions(+), 6 deletions(-) create mode 100644 changelog.d/features/10897-home-recent-requests.md create mode 100644 src/app/(dashboard)/home/HomeRecentRequests.tsx create mode 100644 tests/unit/call-logs-exclude-tests-allowlist.test.ts diff --git a/changelog.d/features/10897-home-recent-requests.md b/changelog.d/features/10897-home-recent-requests.md new file mode 100644 index 0000000000..fd6bcc9abe --- /dev/null +++ b/changelog.d/features/10897-home-recent-requests.md @@ -0,0 +1 @@ +- **feat(home):** add a live **Recent Requests** panel beside the home Provider Topology (polls `GET /api/usage/call-logs?excludeTests=1` every ~3s, gated by the topology appearance toggle + page visibility). `excludeTests` is now an allowlist of real provider inference (`/v1/%` or `/api/v1/%`), applied before `LIMIT`, so connection-test/model-sync/management rows can never leak into the feed ([#10897](https://github.com/diegosouzapw/OmniRoute/pull/10897), extracted from [#8450](https://github.com/diegosouzapw/OmniRoute/pull/8450)) — thanks @nguyenha935 diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index d427f4fcc4..897445fd0c 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -19,6 +19,7 @@ import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron"; import { HomeProviderTopologySection } from "./HomeProviderTopologySection"; import { shouldShowProviderTopologyOnHome } from "./homeAppearance"; +import HomeRecentRequests from "../home/HomeRecentRequests"; type UpdateStep = { step: string; @@ -1126,12 +1127,15 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { )} {showProviderTopologyOnHome && ( - +
+ + +
)} {/* Provider Models Modal */} diff --git a/src/app/(dashboard)/home/HomeRecentRequests.tsx b/src/app/(dashboard)/home/HomeRecentRequests.tsx new file mode 100644 index 0000000000..abe6f57ede --- /dev/null +++ b/src/app/(dashboard)/home/HomeRecentRequests.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Card } from "@/shared/components"; +import { fmtCompact } from "@/shared/utils/formatting"; + +/** + * Home-page "Recent Requests" panel — the live request feed that sits beside the + * Provider Topology graph (parity with 9Router's Usage view). + * + * ## Data source + * Fed by POLLING `GET /api/usage/call-logs?limit=N` every ~3s, NOT by the live + * WebSocket. The WS is used elsewhere for the in-flight beam (it emits + * `request.started` and, since the stuck-latch fix, `request.completed`/`request.failed`), + * but its payload carries no tokens/latency/status and no persisted history — so it + * can't back a "recent requests" table on its own. The call-logs endpoint merges the + * in-memory active/completed entries with persisted rows and returns them newest-first + * (active on top), which is exactly this feed. + * + * The poll is gated by `enabled` (the same `showProviderTopologyOnHome` flag that + * gates the topology section) AND page visibility, so a backgrounded tab pauses. + */ + +const POLL_INTERVAL_MS = 3000; +// Rows shown after client-side filtering of connection-test rows. +const RECENT_LIMIT = 20; +// Fetch a wider window than we display so filtering out connection-test rows +// (a burst of "Test connection" clicks) can't starve the feed below RECENT_LIMIT. +const FETCH_LIMIT = 60; + +type CallLogRow = { + id?: string; + timestamp?: string; + status?: number; + model?: string; + provider?: string; + providerDisplay?: string | null; + path?: string; + sourceFormat?: string | null; + targetFormat?: string | null; + tokens?: { in?: number; out?: number }; + error?: string | null; + active?: boolean; + completed?: boolean; +}; + +/** + * Connection tests write real `call_logs` rows (provider "Test connection" button → + * `/api/providers/[id]/test`) with fixed markers: model `connection-test`, path + * `/api/providers/test`, sourceFormat/targetFormat `test`. Those are health probes, + * not user traffic, so they must not clutter the Recent Requests feed (matching how + * 9Router keeps its Usage list to real calls). Drop any row carrying a test marker. + */ +function isConnectionTestRow(row: CallLogRow): boolean { + return ( + row.model === "connection-test" || + row.sourceFormat === "test" || + row.targetFormat === "test" || + row.path === "/api/providers/test" + ); +} + +type RequestState = "active" | "error" | "ok"; + +function requestState(row: CallLogRow): RequestState { + if (row.active || row.status === 0) return "active"; + if (row.error || (typeof row.status === "number" && row.status >= 400)) return "error"; + return "ok"; +} + +function timeAgo(timestamp: string | undefined, nowMs: number): string { + if (!timestamp) return ""; + const then = Date.parse(timestamp); + if (!Number.isFinite(then)) return ""; + const diff = Math.max(0, Math.floor((nowMs - then) / 1000)); + if (diff < 60) return `${diff}s`; + if (diff < 3600) return `${Math.floor(diff / 60)}m`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h`; + return `${Math.floor(diff / 86400)}d`; +} + +const STATE_DOT: Record = { + active: "bg-primary animate-pulse", + error: "bg-red-500", + ok: "bg-green-500", +}; + +export default function HomeRecentRequests({ enabled = true }: { enabled?: boolean }) { + const t = useTranslations("home"); + const [rows, setRows] = useState([]); + const [loaded, setLoaded] = useState(false); + // A ticking clock so the relative "When" column updates without re-fetching. + const [nowMs, setNowMs] = useState(() => Date.now()); + + useEffect(() => { + if (!enabled) return; + const id = setInterval(() => setNowMs(Date.now()), 1000); + return () => clearInterval(id); + }, [enabled]); + + const load = useCallback(async (signal: AbortSignal) => { + try { + const res = await fetch(`/api/usage/call-logs?limit=${FETCH_LIMIT}&excludeTests=1`, { + cache: "no-store", + signal, + }); + if (!res.ok) return; + const data = await res.json(); + if (signal.aborted) return; + const filtered = Array.isArray(data) + ? (data as CallLogRow[]).filter((row) => !isConnectionTestRow(row)).slice(0, RECENT_LIMIT) + : []; + setRows(filtered); + setLoaded(true); + } catch (error) { + const isAbort = error instanceof DOMException && error.name === "AbortError"; + if (!isAbort) console.error("Failed to load recent requests:", error); + } + }, []); + + useEffect(() => { + if (!enabled) return; + + let cancelled = false; + let timeoutId: ReturnType | null = null; + let controller: AbortController | null = null; + + const tick = async () => { + // Pause polling while the tab is backgrounded; resume on next tick. + if (document.visibilityState === "visible") { + const currentController = new AbortController(); + controller = currentController; + await load(currentController.signal); + if (controller === currentController) controller = null; + } + if (!cancelled) timeoutId = setTimeout(tick, POLL_INTERVAL_MS); + }; + + tick(); + return () => { + cancelled = true; + if (timeoutId) clearTimeout(timeoutId); + controller?.abort(); + }; + }, [enabled, load]); + + return ( + +
+ + {t("recentRequests")} + +
+ + {loaded && rows.length === 0 ? ( +
+ {t("recentRequestsEmpty")} +
+ ) : ( +
+ + + + + + + + + + {rows.map((row, i) => { + const state = requestState(row); + return ( + + + + + + + ); + })} + +
+ {t("recentRequestsModel")} + {t("recentRequestsTokens")} + {t("recentRequestsWhen")}
+ + + {row.model || "—"} + + {fmtCompact(row.tokens?.in)}↑{" "} + {fmtCompact(row.tokens?.out)}↓ + + {state === "active" ? ( + ••• + ) : ( + timeAgo(row.timestamp, nowMs) + )} +
+
+ )} +
+ ); +} diff --git a/src/app/api/usage/call-logs/route.ts b/src/app/api/usage/call-logs/route.ts index 3b3b8ddcc0..0a737ea1f9 100644 --- a/src/app/api/usage/call-logs/route.ts +++ b/src/app/api/usage/call-logs/route.ts @@ -143,6 +143,9 @@ export async function GET(request: Request) { if (searchParams.get("correlationId")) filter.correlationId = searchParams.get("correlationId"); if (searchParams.get("limit")) filter.limit = parseInt(searchParams.get("limit")); if (searchParams.get("offset")) filter.offset = parseInt(searchParams.get("offset")); + // Home Recent Requests feed sets excludeTests=1 so connection-test probe rows + // are dropped at the SQL layer (before LIMIT), not client-side after slicing. + if (searchParams.get("excludeTests") === "1") filter.excludeTests = true; const [logs, connections, providerNodes] = await Promise.all([ getCallLogs(filter), diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 8147dfcb65..bdccf17e23 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1800,6 +1800,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "Reloading page automatically...", "providerTopology": "Provider Topology", + "recentRequests": "Recent Requests", + "recentRequestsEmpty": "No requests yet.", + "recentRequestsModel": "Model", + "recentRequestsTokens": "In / Out", + "recentRequestsWhen": "When", "downloadDmg": "Download DMG (macOS)", "downloadDmgDescription": "A new version of the OmniRoute desktop app is available. Please download and install the macOS DMG installer to update (current: v{version}).", "downloadExe": "Download EXE (Windows)", diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts index cea52ba196..5e3c878c0c 100644 --- a/src/lib/usage/callLogs.ts +++ b/src/lib/usage/callLogs.ts @@ -700,6 +700,14 @@ export async function getCallLogs(filter: any = {}) { if (filter.combo) { conditions.push("cl.combo_name IS NOT NULL"); } + if (filter.excludeTests) { + // Home "Recent Requests" is an allowlist of real provider inference, not a + // blacklist of known backend log types. Persisted provider requests enter via + // the public gateway namespaces (/v1/* or /api/v1/*); internal management work + // (connection tests, model sync, and future /api/providers/* jobs) does not. + // Apply this before LIMIT so backend rows can never displace real traffic. + conditions.push(`(cl.path LIKE '/v1/%' OR cl.path LIKE '/api/v1/%')`); + } if (filter.since) { conditions.push("cl.timestamp >= @since"); params.since = filter.since instanceof Date ? filter.since.toISOString() : String(filter.since); diff --git a/tests/unit/call-logs-exclude-tests-allowlist.test.ts b/tests/unit/call-logs-exclude-tests-allowlist.test.ts new file mode 100644 index 0000000000..78aa5728af --- /dev/null +++ b/tests/unit/call-logs-exclude-tests-allowlist.test.ts @@ -0,0 +1,132 @@ +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"; + +/** + * Home "Recent Requests" feed passes `excludeTests` to getCallLogs so the panel shows + * ONLY real provider inference — never backend/management log rows. The filter is an + * ALLOWLIST of the public gateway namespaces (`/v1/%` and `/api/v1/%`), applied before + * LIMIT, rather than a blacklist of individual known noise types. This guards against + * the reported regression where model-sync rows (request_type 'model-sync', path + * `/api/providers/*`) leaked into the feed because the old blacklist only dropped + * connection-test rows. + */ + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-calllogs-allowlist-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.CALL_LOG_RETENTION_DAYS = "3650"; + +const core = await import("../../src/lib/db/core.ts"); +const callLogs = await import("../../src/lib/usage/callLogs.ts"); + +type SeedRow = { + id: string; + timestamp: string; + path: string; + model: string; + provider: string; + source_format?: string; + request_type?: string | null; +}; + +function insertCallLog(row: SeedRow) { + const db = core.getDbInstance(); + db.prepare( + ` + INSERT INTO call_logs ( + id, timestamp, method, path, status, model, provider, source_format, request_type, detail_state + ) + VALUES ( + @id, @timestamp, 'POST', @path, 200, @model, @provider, @source_format, @request_type, 'none' + ) + ` + ).run({ + source_format: row.source_format ?? null, + request_type: row.request_type ?? null, + ...row, + }); +} + +test.beforeEach(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("excludeTests keeps only /v1 and /api/v1 inference rows, drops all backend/management rows", async () => { + const base = Date.parse("2026-01-01T00:00:00.000Z"); + const iso = (i: number) => new Date(base + i * 1000).toISOString(); + + // Two REAL provider inference rows — the only rows the feed should keep. + insertCallLog({ + id: "real-v1", + timestamp: iso(4), + path: "/v1/chat/completions", + model: "openai/gpt-4.1", + provider: "openai", + }); + insertCallLog({ + id: "real-api-v1", + timestamp: iso(3), + path: "/api/v1/chat/completions", + model: "anthropic/claude-opus-4-8", + provider: "anthropic", + }); + + // Backend/management NOISE — must never appear in the feed. + insertCallLog({ + id: "noise-model-sync", + timestamp: iso(2), + path: "/api/providers/openai/models", + model: "model-sync", + provider: "openai", + source_format: "-", + request_type: "model-sync", + }); + insertCallLog({ + id: "noise-connection-test", + timestamp: iso(1), + path: "/api/providers/test", + model: "connection-test", + provider: "openai", + source_format: "test", + }); + + const rows = await callLogs.getCallLogs({ excludeTests: true, limit: 50 }); + const ids = rows.map((row) => row.id).sort(); + + assert.deepEqual( + ids, + ["real-api-v1", "real-v1"], + "only the /v1 and /api/v1 inference rows survive the allowlist" + ); +}); + +test("without excludeTests every row is returned (allowlist is opt-in)", async () => { + const base = Date.parse("2026-02-01T00:00:00.000Z"); + insertCallLog({ + id: "real", + timestamp: new Date(base).toISOString(), + path: "/v1/chat/completions", + model: "openai/gpt-4.1", + provider: "openai", + }); + insertCallLog({ + id: "sync", + timestamp: new Date(base + 1000).toISOString(), + path: "/api/providers/openai/models", + model: "model-sync", + provider: "openai", + request_type: "model-sync", + }); + + const rows = await callLogs.getCallLogs({ limit: 50 }); + assert.equal(rows.length, 2, "no allowlist → backend rows are not filtered out"); +}); From 9935f80971499a6c98a57573e118c7f84a2c0e20 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 19:34:18 -0300 Subject: [PATCH 114/135] fix(perplexity-web): make the built-in-search hint opt-in (#10904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — extraction of the one still-uncovered fix from #8634 (the other two items — mode "search"→CONCISE downgrade, pplx-opus generation — were already applied on this release tip). typecheck/file-size/changelog/complexity/cognitive-complexity gates all clean, 32/32 tests passing. --- .env.example | 8 +++++ .../fixes/10902-pplx-search-hint-optin.md | 1 + docs/reference/ENVIRONMENT.md | 1 + open-sse/executors/perplexity-web/protocol.ts | 22 +++++++++++--- tests/unit/perplexity-web.test.ts | 29 +++++++++++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/10902-pplx-search-hint-optin.md diff --git a/.env.example b/.env.example index 78d1bd5acf..6a443a9c2d 100644 --- a/.env.example +++ b/.env.example @@ -1415,6 +1415,14 @@ CURSOR_USER_AGENT="Cursor/3.4" # OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000 # OMNIROUTE_PPLX_TLS_GRACE_MS=10000 +# ── Perplexity web: built-in-search hint ── +# Used by: open-sse/executors/perplexity-web/protocol.ts — appends "You have +# built-in web search. Answer questions directly using search results." to the +# caller's system message. Off by default: Perplexity's answer engine searches +# anyway, and for coding clients the sentence leaks into replies as +# meta-commentary. Set to 1/true/yes/on to restore the old behavior. +# OMNIROUTE_PPLX_SEARCH_HINT=0 + # ── Grok web TLS sidecar (Chrome-fingerprinted client) ── # Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the # bogdanfinn/tls-client koffi binding and the JS-side grace window layered on diff --git a/changelog.d/fixes/10902-pplx-search-hint-optin.md b/changelog.d/fixes/10902-pplx-search-hint-optin.md new file mode 100644 index 0000000000..233fb61f19 --- /dev/null +++ b/changelog.d/fixes/10902-pplx-search-hint-optin.md @@ -0,0 +1 @@ +- **fix(perplexity-web):** make the built-in-search hint appended to every system message opt-in via `OMNIROUTE_PPLX_SEARCH_HINT` (off by default) — Perplexity's answer engine searches anyway, and the hint leaked into replies as meta-commentary for coding clients ([#10902](https://github.com/diegosouzapw/OmniRoute/pull/10902), extracted from [#8634](https://github.com/diegosouzapw/OmniRoute/pull/8634)) — thanks @danscMax diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index bf3fbd68d2..3d0e74ff32 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -755,6 +755,7 @@ REQUEST_TIMEOUT_MS (global override) | `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. | +| `OMNIROUTE_PPLX_SEARCH_HINT` | `0` (off) | Appends "You have built-in web search. Answer questions directly using search results." to the caller's system message (`perplexity-web/protocol.ts`). Off by default — Perplexity searches anyway, and the sentence leaks into replies as meta-commentary for coding clients. Set `1`/`true`/`yes`/`on` to restore. | | `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`grokTlsClient.ts`). | | `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | | `OMNIROUTE_NOTION_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`notionTlsClient.ts`); the `notion-web` executor raises it per-request to `180000` for long generations. | diff --git a/open-sse/executors/perplexity-web/protocol.ts b/open-sse/executors/perplexity-web/protocol.ts index f98680c4ff..12e98ccdc4 100644 --- a/open-sse/executors/perplexity-web/protocol.ts +++ b/open-sse/executors/perplexity-web/protocol.ts @@ -370,15 +370,29 @@ export function buildPplxRequestBody( }; } +const SEARCH_HINT = "You have built-in web search. Answer questions directly using search results."; + +/** + * Whether to append {@link SEARCH_HINT} to the caller's system message. + * + * It used to be unconditional. Perplexity's answer engine is search-first anyway, and + * for coding clients the sentence leaks into replies as meta-commentary ("I need to + * search before responding per my instructions"), so it is now opt-in via + * `OMNIROUTE_PPLX_SEARCH_HINT`. Read per call rather than at module load so the flag + * can be flipped without restarting the server (and so tests can toggle it). + */ +function searchHintEnabled(): boolean { + return /^(1|true|yes|on)$/i.test(process.env.OMNIROUTE_PPLX_SEARCH_HINT ?? ""); +} + export function buildQuery(parsed: ParsedMessages, followUpUuid: string | null): string { if (followUpUuid) return parsed.currentMsg; const obj: Record = {}; if (parsed.systemMsg.trim()) { - obj.instructions = [ - parsed.systemMsg.trim(), - "You have built-in web search. Answer questions directly using search results.", - ]; + obj.instructions = searchHintEnabled() + ? [parsed.systemMsg.trim(), SEARCH_HINT] + : [parsed.systemMsg.trim()]; } if (parsed.history.length > 0) { obj.history = parsed.history; diff --git a/tests/unit/perplexity-web.test.ts b/tests/unit/perplexity-web.test.ts index eec23be974..f6f4b7f7a7 100644 --- a/tests/unit/perplexity-web.test.ts +++ b/tests/unit/perplexity-web.test.ts @@ -912,6 +912,35 @@ test("Model mapping: thinking mode uses thinking variant", async () => { } }); +// ─── The search hint is opt-in ────────────────────────────────────────────── +// It used to be appended to every system message and leaked into answers as +// meta-commentary, which is noise for coding clients. + +test("buildQuery: search hint is off by default and opt-in via env", async () => { + const { buildQuery } = await import("../../open-sse/executors/perplexity-web/protocol.ts"); + const parsed = { systemMsg: "You are terse.", history: [], currentMsg: "hi" }; + const HINT = "built-in web search"; + const prev = process.env.OMNIROUTE_PPLX_SEARCH_HINT; + + try { + delete process.env.OMNIROUTE_PPLX_SEARCH_HINT; + const off = JSON.parse(buildQuery(parsed, null)); + assert.deepEqual(off.instructions, ["You are terse."]); + assert.equal(off.query, "hi"); + + process.env.OMNIROUTE_PPLX_SEARCH_HINT = "1"; + const on = JSON.parse(buildQuery(parsed, null)); + assert.equal(on.instructions.length, 2); + assert.ok(on.instructions[1].includes(HINT)); + + process.env.OMNIROUTE_PPLX_SEARCH_HINT = "0"; + assert.equal(JSON.parse(buildQuery(parsed, null)).instructions.length, 1); + } finally { + if (prev === undefined) delete process.env.OMNIROUTE_PPLX_SEARCH_HINT; + else process.env.OMNIROUTE_PPLX_SEARCH_HINT = prev; + } +}); + // ─── Test: Live multi-step stream (no COMPLETED; text_completed + diffs) ──── test("Live multi-step: reconstructs answer without status COMPLETED", async () => { From d9cb4f5f5d692435b81766eb098897e91d4a4809 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:56:34 +0200 Subject: [PATCH 115/135] fix(files): validate the list limit query parameter (#10673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug real: GET /v1/files aceitava limit negativo sem validação (`-5 || 20` avalia truthy em -5, então Math.min(-5, 10000) = -5 passava direto). Agora valida integer/positivo/tamanho e retorna 400 estruturado para valores inválidos, preservando o default 20 e o máximo 10.000. Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/integration/files-api-limit-validation.test.ts — 5/5 passando - tests/integration/files-api.test.ts — 12/12 passando (sem regressão) - tests/unit/batch_api.test.ts teve 1 falha, confirmada DRIFT pré-existente idêntica no tip puro do release (não relacionada, timing de cancelamento de batch) --- src/app/api/v1/files/route.ts | 62 ++++++++++++++- .../files-api-limit-validation.test.ts | 77 +++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 tests/integration/files-api-limit-validation.test.ts diff --git a/src/app/api/v1/files/route.ts b/src/app/api/v1/files/route.ts index 97925dd3b3..2b9d02e49a 100644 --- a/src/app/api/v1/files/route.ts +++ b/src/app/api/v1/files/route.ts @@ -7,6 +7,61 @@ export async function OPTIONS() { return handleCorsOptions(); } +const DEFAULT_LIST_LIMIT = 20; +const MAX_LIST_LIMIT = 10000; + +export function parseFilesListQuery(searchParams: URLSearchParams): + | { + ok: true; + limit: number; + after: string | undefined; + order: "asc" | "desc"; + purpose: string | undefined; + } + | { ok: false; response: Response } { + const rawLimit = searchParams.get("limit"); + let limit = DEFAULT_LIST_LIMIT; + + if (rawLimit !== null) { + if (!/^\d+$/.test(rawLimit)) { + return { + ok: false, + response: NextResponse.json( + { error: { message: "limit must be a positive integer", type: "invalid_request_error" } }, + { status: 400, headers: CORS_HEADERS } + ), + }; + } + + limit = Number.parseInt(rawLimit, 10); + if (limit < 1 || limit > MAX_LIST_LIMIT) { + return { + ok: false, + response: NextResponse.json( + { + error: { + message: `limit must be between 1 and ${MAX_LIST_LIMIT}`, + type: "invalid_request_error", + }, + }, + { status: 400, headers: CORS_HEADERS } + ), + }; + } + } + + const orderParam = searchParams.get("order"); + const order = orderParam === "asc" ? "asc" : "desc"; + + return { + ok: true, + limit, + after: searchParams.get("after") || undefined, + order, + purpose: searchParams.get("purpose") || undefined, + }; +} + export async function POST(request: Request) { const scope = await getApiKeyRequestScope(request); if (scope.rejection) return scope.rejection; @@ -78,10 +133,9 @@ export async function GET(request: Request) { const apiKeyId = scope.apiKeyId; const { searchParams } = new URL(request.url); - const limit = Math.min(Number.parseInt(searchParams.get("limit") || "20") || 20, 10000); - const after = searchParams.get("after") || undefined; - const order = (searchParams.get("order") as "asc" | "desc") || "desc"; - const purpose = searchParams.get("purpose") || undefined; + const parsed = parseFilesListQuery(searchParams); + if (!parsed.ok) return parsed.response; + const { limit, after, order, purpose } = parsed; // We fetch limit + 1 to check if there are more items const files = listFiles({ diff --git a/tests/integration/files-api-limit-validation.test.ts b/tests/integration/files-api-limit-validation.test.ts new file mode 100644 index 0000000000..a697e07967 --- /dev/null +++ b/tests/integration/files-api-limit-validation.test.ts @@ -0,0 +1,77 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { createFile, deleteFile } from "@/lib/db/files"; +import { GET, parseFilesListQuery } from "@/app/api/v1/files/route"; + +describe("GET /v1/files limit validation", () => { + it("defaults to 20 when limit is absent", () => { + const parsed = parseFilesListQuery(new URLSearchParams("order=asc")); + + assert.equal(parsed.ok, true); + if (!parsed.ok) return; + assert.equal(parsed.limit, 20); + }); + + it("parses an explicit positive integer limit", () => { + const parsed = parseFilesListQuery(new URLSearchParams("limit=2&order=asc&purpose=batch")); + + assert.equal(parsed.ok, true); + if (!parsed.ok) return; + assert.equal(parsed.limit, 2); + assert.equal(parsed.order, "asc"); + assert.equal(parsed.purpose, "batch"); + }); + + it("rejects non-integer, zero, and oversized limits", async () => { + for (const rawLimit of ["abc", "1.5", "-1", "0", "10001"]) { + const parsed = parseFilesListQuery( + new URLSearchParams(`limit=${encodeURIComponent(rawLimit)}`) + ); + assert.equal(parsed.ok, false, `limit=${rawLimit} should be rejected`); + if (parsed.ok) continue; + assert.equal(parsed.response.status, 400); + const body = await parsed.response.json(); + assert.equal(body.error.type, "invalid_request_error"); + } + }); + + it("returns only the requested number of files over HTTP", async () => { + const created = [ + createFile({ + bytes: 1, + filename: "test-files-limit-http-a.txt", + purpose: "assistants", + content: Buffer.from("a"), + mimeType: "text/plain", + }), + createFile({ + bytes: 1, + filename: "test-files-limit-http-b.txt", + purpose: "assistants", + content: Buffer.from("b"), + mimeType: "text/plain", + }), + ]; + + try { + const response = await GET( + new Request("http://localhost/v1/files?limit=1&purpose=assistants") + ); + assert.equal(response.status, 200); + const body = await response.json(); + assert.equal(body.object, "list"); + assert.equal(body.data.length, 1); + assert.equal(body.has_more, true); + } finally { + for (const file of created) deleteFile(file.id); + } + }); + + it("returns 400 over HTTP for an invalid limit instead of listing files", async () => { + const response = await GET(new Request("http://localhost/v1/files?limit=-1")); + + assert.equal(response.status, 400); + const body = await response.json(); + assert.equal(body.error.type, "invalid_request_error"); + }); +}); From bc9090ba658f783f68a9430edb418bebf518e84c Mon Sep 17 00:00:00 2001 From: MSiva <113901375+Siva010@users.noreply.github.com> Date: Fri, 21 Aug 2026 04:34:20 +0530 Subject: [PATCH 116/135] fix(translator): merge consecutive same-role contents in direct claudeToGeminiRequest (#10658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug real: a tradução direta claudeToGeminiRequest emitia mensagens consecutivas do mesmo role em contents[], o que a API do Gemini rejeita com HTTP 400 (turnos alternados user/model são obrigatórios). Traz claudeToGeminiRequest à paridade com openaiToGeminiRequest reutilizando mergeConsecutiveSameRoleContents. Validação (worktree combinado a partir de origin/release/v3.8.50, merge limpo, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/claude-to-gemini-consecutive-roles.test.ts — 7/7 passando - tests/unit/claude-to-gemini-budget-tokens-zero-6813.test.ts — 2/2 passando (sem regressão) --- .../claude-to-gemini-consecutive-roles.md | 1 + .../translator/request/claude-to-gemini.ts | 9 +- .../translator/request/openai-to-gemini.ts | 31 +--- .../request/openai-to-gemini/helpers.ts | 26 +++ ...claude-to-gemini-consecutive-roles.test.ts | 150 ++++++++++++++++++ 5 files changed, 190 insertions(+), 27 deletions(-) create mode 100644 changelog.d/fixes/claude-to-gemini-consecutive-roles.md create mode 100644 tests/unit/claude-to-gemini-consecutive-roles.test.ts diff --git a/changelog.d/fixes/claude-to-gemini-consecutive-roles.md b/changelog.d/fixes/claude-to-gemini-consecutive-roles.md new file mode 100644 index 0000000000..17483dce52 --- /dev/null +++ b/changelog.d/fixes/claude-to-gemini-consecutive-roles.md @@ -0,0 +1 @@ +- **fix(translator):** merge consecutive same-role contents in direct Claude to Gemini request translation to prevent upstream HTTP 400 errors diff --git a/open-sse/translator/request/claude-to-gemini.ts b/open-sse/translator/request/claude-to-gemini.ts index b45bf7730a..853e696dbb 100644 --- a/open-sse/translator/request/claude-to-gemini.ts +++ b/open-sse/translator/request/claude-to-gemini.ts @@ -15,6 +15,8 @@ import { getModelSpec } from "../../../src/shared/constants/modelSpecs.ts"; import { buildChangedToolNameMap, buildHistoricalToolResultContext, + mergeConsecutiveSameRoleContents, + type GeminiContent, } from "./openai-to-gemini/helpers.ts"; /** @@ -45,7 +47,7 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) { : null; const result: { model: string; - contents: Array>; + contents: GeminiContent[]; generationConfig: Record; safetySettings: unknown; systemInstruction?: { role: string; parts: Array<{ text: string }> }; @@ -314,6 +316,11 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) { result._toolNameMap = changedToolNameMap; } + // Gemini strictly rejects requests containing consecutive messages with the same role + // (400 INVALID_ARGUMENT: "Request contains consecutive messages with the same role"). + // Normalize adjacent same-role messages by concatenating their parts. + result.contents = mergeConsecutiveSameRoleContents(result.contents); + return result; } diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index cb1cedd713..92399743ff 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -39,8 +39,13 @@ import { escapeHistoricalContextAttribute, escapeHistoricalContextContent, buildHistoricalToolResultContext, + type GeminiPart, + type GeminiContent, + mergeConsecutiveSameRoleContents, } from "./openai-to-gemini/helpers.ts"; +export { mergeConsecutiveSameRoleContents, type GeminiContent, type GeminiPart }; + // Observed Antigravity wrapper output cap, not an underlying model capability. // Keep this bridge-local: Antigravity currently caps visible output around 16K. // See: https://github.com/keisksw/antigravity-output-analysis @@ -56,9 +61,6 @@ const GEMINI_BUILTIN_TOOL_NAMES = new Set([ "googleSearch", ]); -type GeminiPart = Record; -type GeminiContent = { role: string; parts: GeminiPart[] }; - type GeminiFunctionDeclaration = { name: string; description: string; @@ -158,29 +160,6 @@ type GeminiToolNameOptions = { supportsSignatureBypass?: boolean; }; -// Gemini-family APIs (incl. Antigravity / Vertex) reject a `contents[]` array that -// has two adjacent entries with the same role: -// 400 INVALID_ARGUMENT "Request contains consecutive messages with the same role". -// Client history that carries consecutive user turns — or a tool-result turn (mapped -// to role:"user") immediately followed by a plain user turn — would otherwise leak -// that invalid alternation through. Merge adjacent same-role entries by concatenating -// their parts, the same normalization the Kiro and Claude request paths already apply -// (9router#2191). -export function mergeConsecutiveSameRoleContents(contents: GeminiContent[]): GeminiContent[] { - const merged: GeminiContent[] = []; - for (const entry of contents) { - const last = merged[merged.length - 1]; - if (last && last.role === entry.role) { - last.parts.push(...entry.parts); - } else { - // Shallow-copy the entry and its `parts` array so a later same-role merge - // (`last.parts.push(...)`) never mutates the caller's input objects. - merged.push({ ...entry, parts: [...entry.parts] }); - } - } - return merged; -} - // Core: Convert OpenAI request to Gemini format (base for all variants) function openaiToGeminiBase( model: string, diff --git a/open-sse/translator/request/openai-to-gemini/helpers.ts b/open-sse/translator/request/openai-to-gemini/helpers.ts index 810620d8f9..092a857a2c 100644 --- a/open-sse/translator/request/openai-to-gemini/helpers.ts +++ b/open-sse/translator/request/openai-to-gemini/helpers.ts @@ -152,3 +152,29 @@ export function buildHistoricalToolResultContext(name: string, response: unknown "", ].join("\n"); } + +export type GeminiPart = Record; +export type GeminiContent = { role: string; parts: GeminiPart[] }; + +// Gemini-family APIs (incl. Antigravity / Vertex) reject a `contents[]` array that +// has two adjacent entries with the same role: +// 400 INVALID_ARGUMENT "Request contains consecutive messages with the same role". +// Client history that carries consecutive user turns — or a tool-result turn (mapped +// to role:"user") immediately followed by a plain user turn — would otherwise leak +// that invalid alternation through. Merge adjacent same-role entries by concatenating +// their parts, the same normalization the Kiro and Claude request paths already apply +// (9router#2191). +export function mergeConsecutiveSameRoleContents(contents: GeminiContent[]): GeminiContent[] { + const merged: GeminiContent[] = []; + for (const entry of contents) { + const last = merged[merged.length - 1]; + if (last && last.role === entry.role) { + last.parts.push(...entry.parts); + } else { + // Shallow-copy the entry and its `parts` array so a later same-role merge + // (`last.parts.push(...)`) never mutates the caller's input objects. + merged.push({ ...entry, parts: [...entry.parts] }); + } + } + return merged; +} diff --git a/tests/unit/claude-to-gemini-consecutive-roles.test.ts b/tests/unit/claude-to-gemini-consecutive-roles.test.ts new file mode 100644 index 0000000000..eef412fc13 --- /dev/null +++ b/tests/unit/claude-to-gemini-consecutive-roles.test.ts @@ -0,0 +1,150 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { claudeToGeminiRequest } = + await import("../../open-sse/translator/request/claude-to-gemini.ts"); + +test("Claude -> Gemini merges consecutive user text turns into a single user turn", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [ + { role: "user", content: "hello" }, + { role: "user", content: [{ type: "text", text: "world" }] }, + ], + }, + false + ); + + assert.equal(result.contents.length, 1); + assert.equal(result.contents[0].role, "user"); + assert.deepEqual(result.contents[0].parts, [{ text: "hello" }, { text: "world" }]); +}); + +test("Claude -> Gemini merges tool_result and subsequent user instruction into single user turn", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [ + { role: "user", content: "Calculate 2+2" }, + { + role: "assistant", + content: [{ type: "text", text: "I will calculate that." }], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool_call_1", + content: "4", + }, + ], + }, + { + role: "user", + content: "Now add 10 to that result", + }, + ], + }, + false + ); + + // Contents must alternate properly and not have consecutive same-role turns + for (let i = 1; i < result.contents.length; i++) { + assert.notEqual( + result.contents[i].role, + result.contents[i - 1].role, + `Consecutive same-role detected at index ${i - 1} and ${i}: ${result.contents[i].role}` + ); + } + + // The last turn should be a merged user turn containing both the tool context and the text + const lastTurn = result.contents[result.contents.length - 1]; + assert.equal(lastTurn.role, "user"); + assert.equal(lastTurn.parts.length, 2); + assert.ok( + typeof (lastTurn.parts[0] as { text: string }).text === "string" && + (lastTurn.parts[0] as { text: string }).text.includes("previous_tool_result_context") + ); + assert.deepEqual(lastTurn.parts[1], { text: "Now add 10 to that result" }); +}); + +test("Claude -> Gemini preserves alternating conversation turns without spurious merging", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi! How can I help?" }, + { role: "user", content: "What is the capital of France?" }, + ], + }, + false + ); + + assert.equal(result.contents.length, 3); + assert.equal(result.contents[0].role, "user"); + assert.deepEqual(result.contents[0].parts, [{ text: "Hello" }]); + assert.equal(result.contents[1].role, "model"); + assert.deepEqual(result.contents[1].parts, [{ text: "Hi! How can I help?" }]); + assert.equal(result.contents[2].role, "user"); + assert.deepEqual(result.contents[2].parts, [{ text: "What is the capital of France?" }]); +}); + +test("Claude -> Gemini merges three or more consecutive user turns into a single user turn", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [ + { role: "user", content: "part 1" }, + { role: "user", content: "part 2" }, + { role: "user", content: "part 3" }, + ], + }, + false + ); + + assert.equal(result.contents.length, 1); + assert.equal(result.contents[0].role, "user"); + assert.deepEqual(result.contents[0].parts, [ + { text: "part 1" }, + { text: "part 2" }, + { text: "part 3" }, + ]); +}); + +test("Claude -> Gemini handles empty messages array without error", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [], + }, + false + ); + + assert.deepEqual(result.contents, []); +}); + +test("Claude -> Gemini merges consecutive assistant turns into a single model turn", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: "response part 1" }, + { role: "assistant", content: [{ type: "text", text: "response part 2" }] }, + ], + }, + false + ); + + assert.equal(result.contents.length, 2); + assert.equal(result.contents[0].role, "user"); + assert.deepEqual(result.contents[0].parts, [{ text: "hello" }]); + assert.equal(result.contents[1].role, "model"); + assert.deepEqual(result.contents[1].parts, [ + { text: "response part 1" }, + { text: "response part 2" }, + ]); +}); From eb6f3197122e60c39f3947e63b56770a6595d40a Mon Sep 17 00:00:00 2001 From: Yawar Date: Fri, 21 Aug 2026 04:54:42 +0530 Subject: [PATCH 117/135] feat(providers): add tabitoken gateway and serve hcnsec's four protocols (#10668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — PR muito bem documentado e verificado. Adiciona o gateway TabiToken (Anthropic-first, /v1/messages, x-api-key) e estende hcnsec de 1 para 4 protocolos (Chat, Responses, Anthropic Messages, Gemini). AlternateFormat ganha o hook urlBuilder opcional (necessário para o path model-scoped do Gemini), compartilhado com o provider gemini nativo em vez de duplicado. Reconciliado nesta sessão contra o release tip atualizado (base drift real: 343→345 canônicos entre quando o PR foi criado e o merge, mais os PRs #10673/#10658 mergeados nesse meio-tempo). Conflitos em contagens de providers (docs, file-size baseline, teste de partição) resolvidos additivamente. Validação (reconciliação a partir de origin/release/v3.8.50): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - npm run check:provider-consistency — OK (266 REGISTRY entries, 346 providers canônicos, 0 exceções) - 40/40 testes passando (newapi-gateway-providers, hcnsec-provider, providers-constants-split, alternate-formats) --- AGENTS.md | 2 +- README.md | 14 +- .../10668-newapi-gateway-protocols.md | 2 + config/quality/file-size-baseline.json | 5 +- docs/reference/PROVIDER_REFERENCE.md | 5 +- llm.txt | 8 +- open-sse/config/providers/alternateFormats.ts | 13 + open-sse/config/providers/index.ts | 2 + .../config/providers/registry/gemini/index.ts | 7 +- .../config/providers/registry/hcnsec/index.ts | 53 +++- .../providers/registry/tabitoken/index.ts | 59 ++++ open-sse/config/providers/shared.ts | 17 + open-sse/executors/default.ts | 3 + package.json | 2 +- src/shared/constants/config.ts | 1 + src/shared/constants/providers.ts | 1 + .../constants/providers/apikey/gateways.ts | 15 + tests/snapshots/provider/translate-path.json | 26 ++ tests/unit/newapi-gateway-providers.test.ts | 299 ++++++++++++++++++ tests/unit/providers-constants-split.test.ts | 12 +- 20 files changed, 517 insertions(+), 29 deletions(-) create mode 100644 changelog.d/features/10668-newapi-gateway-protocols.md create mode 100644 open-sse/config/providers/registry/tabitoken/index.ts create mode 100644 tests/unit/newapi-gateway-providers.test.ts diff --git a/AGENTS.md b/AGENTS.md index 2168ae70b9..30e1f6d27c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below. ## Project at a Glance -**OmniRoute** — unified AI proxy/router. One endpoint, 343 LLM providers, auto-fallback. +**OmniRoute** — unified AI proxy/router. One endpoint, 346 LLM providers, auto-fallback. | Layer | Location | Purpose | | ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/README.md b/README.md index fc66a61a71..2293040ff3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # 🚀 OmniRoute — The Free AI Gateway -OmniRoute — Never stop coding. Every AI tool → 343 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 343 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start. +OmniRoute — Never stop coding. Every AI tool → 346 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 346 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start.
@@ -101,7 +101,7 @@ ⚙️ Features
🎯 Combos - 🌐 Providers + 🌐 Providers 🔌 CLI & MCP @@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \ -The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests). +The Promise — One endpoint. 346 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 346 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests).

@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step: -What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 343 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs. +What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 346 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs. 📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md) @@ -559,7 +559,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute - **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md) - **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md) - **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md) -- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **343-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md) +- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **346-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md) - **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md) - **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md) @@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
-## 🌐 343 AI Providers — 90+ Free +## 🌐 346 AI Providers — 90+ Free
-> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **56 free forever**. +> The most complete catalog of any open-source router: **346 providers**, **90+ with a free tier**, **56 free forever**.
diff --git a/changelog.d/features/10668-newapi-gateway-protocols.md b/changelog.d/features/10668-newapi-gateway-protocols.md new file mode 100644 index 0000000000..1ec6e5f0b7 --- /dev/null +++ b/changelog.d/features/10668-newapi-gateway-protocols.md @@ -0,0 +1,2 @@ +- **feat(providers):** add the TabiToken NewAPI gateway (`tabitoken`) and teach the existing HCNSec entry (`hcnsec`) the three further protocols it actually serves. TabiToken leaves the NewAPI pricing endpoint public, so its catalog is read from the host rather than guessed: four Claude models, each reporting the Anthropic and OpenAI protocols. HCNSec shipped OpenAI-only; probing the host showed `/v1/messages`, `/v1/responses` and the Gemini `/v1beta` path all reach its token layer, so each is now declared as an alternate format — with its default format, base URL, auth scheme and regional catalog classification untouched. ([#10668](https://github.com/diegosouzapw/OmniRoute/pull/10668)) — thanks @yawar-aquil +- **feat(sse):** allow an alternate protocol to build its own upstream URL. `AlternateFormat` gained an optional `urlBuilder`, because the Gemini protocol carries the model inside the path (`{base}/{model}:generateContent`) and the existing `chatPath`/`urlSuffix` fields are constants that cannot express it. The route builder is extracted as `buildGeminiGenerateContentUrl` and shared with the native `gemini` provider so the two consumers cannot drift on the `?alt=sse` streaming suffix. ([#10668](https://github.com/diegosouzapw/OmniRoute/pull/10668)) — thanks @yawar-aquil diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 900f6220f8..7a65a56808 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -444,14 +444,15 @@ "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1062, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051, "src/shared/components/ModelSelectModal.tsx": 1138, - "src/shared/constants/providers/apikey/gateways.ts": 1268, + "src/shared/constants/providers/apikey/gateways.ts": 1283, "open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387, "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "src/lib/modelCapabilities.ts": 1006, "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014, "open-sse/config/imageRegistry.ts": 1034, "src/sse/handlers/chatHelpers.ts": 1017, - "src/shared/middleware/chatBodyAdmission.ts": 1005 + "src/shared/middleware/chatBodyAdmission.ts": 1005, + "_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file)." }, "_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.", "_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).", diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index e4f3c4ad58..30eac06ad5 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -10,7 +10,7 @@ lastUpdated: 2026-08-20 > Regenerate with: `npm run gen:provider-reference` > **Last generated:** 2026-08-20 -Total providers: **343**. See category breakdown below. +Total providers: **346**. See category breakdown below. ## Categories @@ -120,7 +120,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — | | `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — | -## API Key Providers (paid / paid-with-free-credits) (230) +## API Key Providers (paid / paid-with-free-credits) (231) | ID | Alias | Name | Tags | Website | Notes | |----|-------|------|------|---------|-------| @@ -319,6 +319,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `sumopod` | `sumopod` | SumoPod | API key | [link](https://ai.sumopod.com) | Use your SumoPod API key (sk-...) in Authorization: Bearer . Fully OpenAI-compatible. API base URL: https://ai.sumopod.com/v1. | | `suno` | `suno` | Suno | API key | [link](https://suno.ai) | Paste session cookie from suno.ai (Clerk auth) | | `synthetic` | `synthetic` | Synthetic | API key, aggregator | [link](https://synthetic.new) | — | +| `tabitoken` | `tabitoken` | TabiToken | API key, aggregator | [link](https://tabitoken.com) | — | | `tencent` | `tencent` | Tencent Hunyuan | API key | [link](https://hunyuan.tencent.com) | Get API key at console.cloud.tencent.com | | `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. | | `tinyfish` | `tf` | TinyFish Fetch | API key | [link](https://docs.tinyfish.ai/fetch-api) | X-API-Key from agent.tinyfish.ai/api-keys | diff --git a/llm.txt b/llm.txt index c80df65ca7..15a03831d1 100644 --- a/llm.txt +++ b/llm.txt @@ -1,6 +1,6 @@ # OmniRoute -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -165,7 +165,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **343 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -475,7 +475,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/open-sse/config/providers/alternateFormats.ts b/open-sse/config/providers/alternateFormats.ts index b223a26448..8a7deb782c 100644 --- a/open-sse/config/providers/alternateFormats.ts +++ b/open-sse/config/providers/alternateFormats.ts @@ -19,6 +19,19 @@ export interface AlternateFormat { authHeader?: string; headers?: Record; urlSuffix?: string; + /** + * Monta a URL final quando o protocolo alternativo embute o modelo no path, e + * nao apenas um sufixo fixo. O caso concreto e o protocolo Gemini, cuja rota e + * `{base}/{model}:generateContent` (ou `:streamGenerateContent?alt=sse`) — algo + * que `chatPath`/`urlSuffix` nao expressam, porque ambos sao constantes. + * + * Mesma assinatura do `urlBuilder` de RegistryEntry (base ja sem "/" final, + * modelo e stream), de proposito: um gateway que fala Gemini como alternativa + * reaproveita `buildGeminiGenerateContentUrl` de shared.ts — o mesmo builder que + * o provedor Gemini nativo usa — em vez de reimplementar a rota. + * Quando ausente, a URL continua sendo `baseUrl + chatPath + urlSuffix`. + */ + urlBuilder?: (base: string, model: string, stream: boolean) => string; label: string; } diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index c098dbf29f..a7067a022e 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -263,6 +263,7 @@ import { freeinferenceProvider } from "./registry/freeinference/index.ts"; import { freeAiProvider } from "./registry/free-ai/index.ts"; import { voidAiProvider } from "./registry/void-ai/index.ts"; import { helixmindProvider } from "./registry/helixmind/index.ts"; +import { tabitokenProvider } from "./registry/tabitoken/index.ts"; export const REGISTRY: Record = { aimlapi: aimlapiProvider, @@ -530,4 +531,5 @@ export const REGISTRY: Record = { "free-ai": freeAiProvider, "void-ai": voidAiProvider, helixmind: helixmindProvider, + tabitoken: tabitokenProvider, }; diff --git a/open-sse/config/providers/registry/gemini/index.ts b/open-sse/config/providers/registry/gemini/index.ts index 8119ed9c41..468fcd8889 100644 --- a/open-sse/config/providers/registry/gemini/index.ts +++ b/open-sse/config/providers/registry/gemini/index.ts @@ -1,5 +1,5 @@ import type { RegistryEntry } from "../../shared.ts"; -import { resolvePublicCred } from "../../shared.ts"; +import { buildGeminiGenerateContentUrl, resolvePublicCred } from "../../shared.ts"; export const geminiProvider: RegistryEntry = { id: "gemini", @@ -7,10 +7,7 @@ export const geminiProvider: RegistryEntry = { format: "gemini", executor: "default", baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", - urlBuilder: (base, model, stream) => { - const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; - return `${base}/${model}:${action}`; - }, + urlBuilder: buildGeminiGenerateContentUrl, authType: "apikey", authHeader: "x-goog-api-key", defaultContextLength: 1048576, diff --git a/open-sse/config/providers/registry/hcnsec/index.ts b/open-sse/config/providers/registry/hcnsec/index.ts index acce62c2bc..dba2b690da 100644 --- a/open-sse/config/providers/registry/hcnsec/index.ts +++ b/open-sse/config/providers/registry/hcnsec/index.ts @@ -1,11 +1,62 @@ import type { RegistryEntry } from "../../shared.ts"; -import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; +import { + buildGeminiGenerateContentUrl, + buildOpenAiCompatibleRegistryEntry, + getAnthropicCompatHeaders, +} from "../../shared.ts"; +/** + * HCNSec — NewAPI-based host (https://api.hcnsec.cn), announced by its own `/api/status` as + * 新疆幻城网安科技公益大模型安全网关. Catalogued as an API-key **regional** provider + * (`APIKEY_PROVIDERS_REGIONAL.hcnsec`); this entry only describes how to reach it. + * + * It shipped OpenAI-only. The three alternates below were added after probing the host live: + * every one of them reaches the NewAPI token layer (`{"error":{"type":"new_api_error"}}` on an + * invalid key) rather than a router 404, so each is a route this host actually serves — + * including the Gemini path in both its unary and `:streamGenerateContent?alt=sse` forms. + * The default format, base URL and auth scheme are deliberately untouched. + * + * `models: []` is unchanged and deliberate. Unlike TabiToken, this host gates every discovery + * endpoint behind auth (`/api/status` reports `pricing.requireAuth: true`; `/api/pricing`, + * `/api/models`, `/api/models/display` and `/api/user/models` all answer "Unauthorized, not + * logged in and no access token provided"). Rather than ship a guessed catalog, the model list + * is left to live discovery through `modelsUrl` with the operator's own key — the same + * arrangement `anyapi` and `helixmind` use. + */ export const hcnsecProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ id: "hcnsec", alias: "hcnsec", baseUrl: "https://api.hcnsec.cn/v1/chat/completions", modelsUrl: "https://api.hcnsec.cn/v1/models", + responsesBaseUrl: "https://api.hcnsec.cn/v1/responses", models: [], passthroughModels: true, + alternateFormats: [ + { + // `Anthropic-Version` is scoped to this alternate (deepseek's arrangement) because + // it is only meaningful on `/v1/messages`, and because `default.ts` supplies that + // default solely for `anthropic-compatible-*` provider ids — not for a gateway that + // reaches the Claude protocol through an alternate. + format: "claude", + baseUrl: "https://api.hcnsec.cn/v1/messages", + authHeader: "x-api-key", + headers: getAnthropicCompatHeaders(), + label: "Anthropic-compatible", + }, + { + format: "openai-responses", + baseUrl: "https://api.hcnsec.cn/v1/responses", + authHeader: "bearer", + label: "OpenAI Responses", + }, + { + // The Gemini protocol carries the model in the path, so this alternate needs the + // same builder the native `gemini` provider uses instead of a constant chatPath. + format: "gemini", + baseUrl: "https://api.hcnsec.cn/v1beta/models", + authHeader: "x-goog-api-key", + urlBuilder: buildGeminiGenerateContentUrl, + label: "Gemini-compatible", + }, + ], }); diff --git a/open-sse/config/providers/registry/tabitoken/index.ts b/open-sse/config/providers/registry/tabitoken/index.ts new file mode 100644 index 0000000000..f95d188c20 --- /dev/null +++ b/open-sse/config/providers/registry/tabitoken/index.ts @@ -0,0 +1,59 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { getAnthropicCompatHeaders } from "../../shared.ts"; + +/** + * TabiToken — NewAPI-based Claude gateway (https://tabitoken.com). + * + * The catalog below is not hand-written: TabiToken leaves the NewAPI pricing endpoint + * public (`/api/status` reports `pricing.requireAuth: false`), so `GET /api/pricing` + * lists every model together with the protocols it accepts. All four entries report + * `supported_endpoint_types: ["anthropic","openai"]`, which is why only those two + * protocols are declared here — the host also routes `/v1/responses` and the Gemini + * `/v1beta` path, but no model on this gateway is reachable through them. + * + * Claude-first (`/v1/messages` + `x-api-key`) because the whole catalog is Claude and + * that avoids a translation hop for Claude-native clients; `passthroughModels` keeps + * models added upstream usable before this list catches up. + * + * No static fingerprint headers. TabiToken fronts Cloudflare, and the only User-Agent + * it rejects is the literal `curl/*` default — a browser UA is answered with + * "Access denied: abusive or non-compliant use is prohibited", while sending no UA + * (the fetch default) reaches the token layer normally. So, unlike agentrouter, this + * entry needs neither a static nor a dynamic wire image. + * + * `headers` carries only `Anthropic-Version`, and it has to live on the entry rather + * than come from the executor: `default.ts` defaults that header solely for provider + * ids prefixed `anthropic-compatible-` (buildHeaders, the `startsWith` branch), so a + * plain `format: "claude"` entry would POST `/v1/messages` without it. Six sibling + * third-party Claude entries (wafer, zai, xiaomi-mimo, xiaomi-mimo-token-plan, + * bailian-coding-plan, deepseek) set it for exactly this reason. Entry-level headers + * are merged for every format (base.ts::buildHeadersPreamble), so the OpenAI alternate + * below also sends it — a documented no-op on `/chat/completions` (see the same note in + * executors/github.ts). + */ +export const tabitokenProvider: RegistryEntry = { + id: "tabitoken", + alias: "tabitoken", + format: "claude", + executor: "default", + baseUrl: "https://tabitoken.com/v1/messages", + modelsUrl: "https://tabitoken.com/v1/models", + authType: "apikey", + authHeader: "x-api-key", + headers: getAnthropicCompatHeaders(), + alternateFormats: [ + { + format: "openai", + baseUrl: "https://tabitoken.com/v1/chat/completions", + authHeader: "bearer", + label: "OpenAI-compatible", + }, + ], + models: [ + { id: "claude-opus-5", name: "Claude Opus 5" }, + { id: "claude-opus-5-thinking", name: "Claude Opus 5 (Thinking)" }, + { id: "claude-opus-4-8", name: "Claude Opus 4.8" }, + { id: "claude-opus-4-8-thinking", name: "Claude Opus 4.8 (Thinking)" }, + ], + passthroughModels: true, +}; diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 94e39b29a0..d65a0240e0 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -758,3 +758,20 @@ export function buildAntigravityUrl(base: string, model: string, stream: boolean const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent"; return `${base}${path}`; } + +/** + * Gemini protocol `generateContent` route: the model goes in the path, not the body. + * + * Shared because the format has two consumers: the native `gemini` provider + * (RegistryEntry.urlBuilder) and gateways that expose Gemini as an alternate + * protocol (AlternateFormat.urlBuilder, see alternateFormats.ts). One copy per + * consumer would leave the streaming `?alt=sse` suffix free to diverge. + */ +export function buildGeminiGenerateContentUrl( + base: string, + model: string, + stream: boolean +): string { + const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; + return `${base}/${model}:${action}`; +} diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 056772040a..0d2c9182bd 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -191,6 +191,9 @@ export class DefaultExecutor extends BaseExecutor { // Operator's manual override (#6147) keeps its own semantics and falls // through to the provider-specific handling below. const normalized = alternate.baseUrl.replace(/\/$/, ""); + // A model-scoped alternate (the Gemini protocol: `{base}/{model}:generateContent`) + // builds its own URL — chatPath/urlSuffix are constants and cannot carry the model. + if (alternate.urlBuilder) return alternate.urlBuilder(normalized, model, stream); return `${normalized}${alternate.chatPath || ""}${alternate.urlSuffix || ""}`; } } diff --git a/package.json b/package.json index fdacc42ef4..8e9db759ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "omniroute", "version": "3.8.50", - "description": "Unified AI router with 343 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", + "description": "Unified AI router with 346 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": { "omniroute": "bin/omniroute.mjs", diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index 9b1bbcb132..0810042444 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -36,6 +36,7 @@ export const PROVIDER_ENDPOINTS = { "free-ai": "https://api.free.ai/v1/chat/", "void-ai": "https://api.voidai.app/v1/chat/completions", helixmind: "https://helixmind.online/v1/chat/completions", + tabitoken: "https://tabitoken.com/v1/messages", glm: "https://api.z.ai/api/anthropic/v1/messages", glmt: "https://api.z.ai/api/anthropic/v1/messages", "bailian-coding-plan": diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 37174092b2..d5754eeea4 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -146,6 +146,7 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([ "free-ai", "void-ai", "helixmind", + "tabitoken", ]); export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([ diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 2b9c077e15..1f2bd59979 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -1264,4 +1264,19 @@ export const APIKEY_PROVIDERS_GATEWAYS = { apiHint: "Create a helix- key and use https://helixmind.online/v1. OpenAI requests use Bearer authentication; the Anthropic-compatible messages endpoint accepts x-api-key.", }, + // TabiToken (https://tabitoken.com) — NewAPI-based Claude gateway. Its public pricing + // endpoint lists a Claude-only catalog (Opus 5 / 4.8, each with a -thinking variant), + // every model accepting the Anthropic and OpenAI protocols. + tabitoken: { + id: "tabitoken", + alias: "tabitoken", + name: "TabiToken", + icon: "hub", + color: "#F97316", + textIcon: "TT", + passthroughModels: true, + website: "https://tabitoken.com", + apiHint: + "Create an sk- key at https://tabitoken.com and use https://tabitoken.com. The Anthropic-compatible /v1/messages endpoint (default) takes x-api-key; /v1/chat/completions takes Bearer.", + }, }; diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 917bc7d086..0afdd55ac3 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -5368,6 +5368,32 @@ "stream": "https://t3.chat/api/chat" } }, + "tabitoken": { + "format": "claude", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Anthropic-Version": "2023-06-01", + "Content-Type": "application/json", + "x-api-key": "" + }, + "nonStream": { + "Anthropic-Version": "2023-06-01", + "Content-Type": "application/json", + "x-api-key": "" + }, + "oauth": { + "Accept": "text/event-stream", + "Anthropic-Version": "2023-06-01", + "Content-Type": "application/json", + "x-api-key": "" + } + }, + "url": { + "nonStream": "https://tabitoken.com/v1/messages", + "stream": "https://tabitoken.com/v1/messages" + } + }, "tencent": { "format": "openai", "headers": { diff --git a/tests/unit/newapi-gateway-providers.test.ts b/tests/unit/newapi-gateway-providers.test.ts new file mode 100644 index 0000000000..c2e960f42d --- /dev/null +++ b/tests/unit/newapi-gateway-providers.test.ts @@ -0,0 +1,299 @@ +// Coverage for the two NewAPI-based gateways touched alongside the AlternateFormat.urlBuilder +// hook: tabitoken (new — Claude-first, 2 protocols) and hcnsec (already shipped as an +// OpenAI-only regional entry, now declaring the 3 further protocols it actually serves). +// +// hcnsec is the first registry entry to expose the **Gemini** protocol as an alternate, and the +// Gemini route embeds the model in the path (`{base}/{model}:generateContent`) instead of a +// constant suffix. `chatPath`/`urlSuffix` cannot express that, so AlternateFormat grew an +// optional `urlBuilder` and shared.ts grew `buildGeminiGenerateContentUrl` — the same builder the +// native `gemini` provider now uses. The identity assertion at the bottom of this file is what +// keeps those two consumers from drifting apart. +// +// hcnsec's pre-existing guarantees (openai format, bearer auth, no static model seed) keep their +// own guard in tests/unit/hcnsec-provider.test.ts; the shape test here re-asserts them only to +// prove the alternates were added *without* moving the defaults. +// +// Everything is asserted through the real DefaultExecutor (not a reimplementation of the +// precedence rules) because both providers exist in the static registry — the limitation the +// older Task 3/4 tests in alternate-formats.test.ts had to work around no longer applies. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { DefaultExecutor } from "../../open-sse/executors/default.ts"; +import { getTargetFormat } from "../../open-sse/services/provider.ts"; +import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; +import { buildGeminiGenerateContentUrl } from "../../open-sse/config/providers/shared.ts"; +import { geminiProvider } from "../../open-sse/config/providers/registry/gemini/index.ts"; +import { getAlternateFormats } from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts"; +import { AI_PROVIDERS, AGGREGATOR_PROVIDER_IDS } from "../../src/shared/constants/providers.ts"; +import { APIKEY_PROVIDERS_GATEWAYS } from "../../src/shared/constants/providers/apikey/gateways.ts"; +import { APIKEY_PROVIDERS_REGIONAL } from "../../src/shared/constants/providers/apikey/regional.ts"; +import { PROVIDER_ENDPOINTS } from "../../src/shared/constants/config.ts"; + +const KEY = { apiKey: "sk-test" } as never; +const withFormat = (targetFormat: string) => + ({ apiKey: "sk-test", providerSpecificData: { targetFormat } }) as never; + +// ── tabitoken ───────────────────────────────────────────────────────────────── + +test("tabitoken registry entry is Claude-first with an OpenAI alternate", () => { + const entry = getRegistryEntry("tabitoken"); + assert.equal(entry?.format, "claude"); + assert.equal(entry?.executor, "default"); + assert.equal(entry?.authType, "apikey"); + assert.equal(entry?.authHeader, "x-api-key"); + assert.equal(entry?.baseUrl, "https://tabitoken.com/v1/messages"); + assert.equal(entry?.modelsUrl, "https://tabitoken.com/v1/models"); + assert.equal(entry?.passthroughModels, true); + // The generic claude-format path in default.ts::buildHeaders only defaults + // anthropic-version for `anthropic-compatible-*` ids, so the entry carries it. + assert.equal(entry?.headers?.["Anthropic-Version"], "2023-06-01"); + // Only the two protocols tabitoken's own /api/pricing reports per model + // (supported_endpoint_types: ["anthropic","openai"]). + assert.deepEqual( + (entry?.alternateFormats || []).map((a) => a.format), + ["openai"] + ); +}); + +test("tabitoken catalog matches the four Claude models its public pricing endpoint lists", () => { + const entry = getRegistryEntry("tabitoken"); + assert.deepEqual( + (entry?.models || []).map((m) => m.id), + ["claude-opus-5", "claude-opus-5-thinking", "claude-opus-4-8", "claude-opus-4-8-thinking"] + ); + for (const model of entry?.models || []) { + assert.equal(typeof model.name, "string"); + assert.ok(model.name.length > 0, `${model.id} must carry a display name`); + } +}); + +test("tabitoken defaults to /v1/messages + x-api-key and switches to Bearer on the OpenAI alternate", () => { + const executor = new DefaultExecutor("tabitoken"); + + assert.equal(getTargetFormat("tabitoken", null), "claude"); + assert.equal( + executor.buildUrl("claude-opus-5", true, 0, KEY), + "https://tabitoken.com/v1/messages" + ); + const claudeHeaders = executor.buildHeaders(KEY, true) as Record; + assert.equal(claudeHeaders["x-api-key"], "sk-test"); + assert.equal(claudeHeaders["Authorization"], undefined); + assert.equal(claudeHeaders["Anthropic-Version"], "2023-06-01"); + + assert.equal(getTargetFormat("tabitoken", { targetFormat: "openai" }), "openai"); + const openaiCreds = withFormat("openai"); + assert.equal( + executor.buildUrl("claude-opus-5", true, 0, openaiCreds), + "https://tabitoken.com/v1/chat/completions" + ); + const openaiHeaders = executor.buildHeaders(openaiCreds, true) as Record; + assert.equal(openaiHeaders["Authorization"], "Bearer sk-test"); + assert.equal(openaiHeaders["x-api-key"], undefined); +}); + +test("tabitoken ignores a targetFormat it does not declare", () => { + const executor = new DefaultExecutor("tabitoken"); + // Unknown alternate → resolveAlternateFormat returns null → default format/route/auth. + assert.equal(getTargetFormat("tabitoken", { targetFormat: "gemini" }), "claude"); + const creds = withFormat("gemini"); + assert.equal( + executor.buildUrl("claude-opus-5", true, 0, creds), + "https://tabitoken.com/v1/messages" + ); + const headers = executor.buildHeaders(creds, true) as Record; + assert.equal(headers["x-api-key"], "sk-test"); + assert.equal(headers["x-goog-api-key"], undefined); +}); + +// ── hcnsec ──────────────────────────────────────────────────────────────────── + +test("hcnsec keeps its OpenAI-first defaults and adds the three further protocols it serves", () => { + const entry = getRegistryEntry("hcnsec"); + // Unchanged by this extension — the guard that adding alternates moved no default. + assert.equal(entry?.format, "openai"); + assert.equal(entry?.executor, "default"); + assert.equal(entry?.authHeader, "bearer"); + assert.equal(entry?.baseUrl, "https://api.hcnsec.cn/v1/chat/completions"); + assert.equal(entry?.modelsUrl, "https://api.hcnsec.cn/v1/models"); + assert.deepEqual(entry?.models, []); + assert.equal(entry?.passthroughModels, true); + // Added: the Responses route plus one alternate per further protocol. + assert.equal(entry?.responsesBaseUrl, "https://api.hcnsec.cn/v1/responses"); + assert.deepEqual( + (entry?.alternateFormats || []).map((a) => a.format), + ["claude", "openai-responses", "gemini"] + ); +}); + +test("hcnsec routes each protocol to its own endpoint with the matching auth scheme", () => { + const executor = new DefaultExecutor("hcnsec"); + + assert.equal(getTargetFormat("hcnsec", null), "openai"); + assert.equal( + executor.buildUrl("gpt-5", true, 0, KEY), + "https://api.hcnsec.cn/v1/chat/completions" + ); + assert.equal( + (executor.buildHeaders(KEY, true) as Record)["Authorization"], + "Bearer sk-test" + ); + + const claudeCreds = withFormat("claude"); + assert.equal(getTargetFormat("hcnsec", { targetFormat: "claude" }), "claude"); + assert.equal( + executor.buildUrl("claude-opus-5", true, 0, claudeCreds), + "https://api.hcnsec.cn/v1/messages" + ); + const claudeHeaders = executor.buildHeaders(claudeCreds, true) as Record; + assert.equal(claudeHeaders["x-api-key"], "sk-test"); + assert.equal(claudeHeaders["Authorization"], undefined); + assert.equal(claudeHeaders["Anthropic-Version"], "2023-06-01"); + + const responsesCreds = withFormat("openai-responses"); + assert.equal(getTargetFormat("hcnsec", { targetFormat: "openai-responses" }), "openai-responses"); + assert.equal( + executor.buildUrl("gpt-5", true, 0, responsesCreds), + "https://api.hcnsec.cn/v1/responses" + ); + assert.equal( + (executor.buildHeaders(responsesCreds, true) as Record)["Authorization"], + "Bearer sk-test" + ); +}); + +test("hcnsec's Gemini alternate builds the model-scoped generateContent route in both forms", () => { + const executor = new DefaultExecutor("hcnsec"); + const creds = withFormat("gemini"); + + assert.equal(getTargetFormat("hcnsec", { targetFormat: "gemini" }), "gemini"); + // Unary: the model lands in the path, which is exactly what chatPath/urlSuffix + // (both constants) could not express before urlBuilder existed. + assert.equal( + executor.buildUrl("gemini-3.7-flash", false, 0, creds), + "https://api.hcnsec.cn/v1beta/models/gemini-3.7-flash:generateContent" + ); + // Streaming keeps the `?alt=sse` suffix the Gemini protocol requires. + assert.equal( + executor.buildUrl("gemini-3.7-flash", true, 0, creds), + "https://api.hcnsec.cn/v1beta/models/gemini-3.7-flash:streamGenerateContent?alt=sse" + ); + + const headers = executor.buildHeaders(creds, true) as Record; + assert.equal(headers["x-goog-api-key"], "sk-test"); + assert.equal(headers["Authorization"], undefined); + assert.equal(headers["x-api-key"], undefined); +}); + +test("hcnsec ignores a targetFormat it does not declare", () => { + const executor = new DefaultExecutor("hcnsec"); + assert.equal(getTargetFormat("hcnsec", { targetFormat: "codex" }), "openai"); + const creds = withFormat("codex"); + assert.equal( + executor.buildUrl("gpt-5", true, 0, creds), + "https://api.hcnsec.cn/v1/chat/completions" + ); + assert.equal( + (executor.buildHeaders(creds, true) as Record)["Authorization"], + "Bearer sk-test" + ); +}); + +// ── shared Gemini URL builder ───────────────────────────────────────────────── + +test("the Gemini route builder is shared with the native gemini provider, not duplicated", () => { + // Identity, not equality: if someone re-inlines an arrow function on either side the + // `?alt=sse` suffix is free to drift between the two consumers. This is the guard. + assert.equal(geminiProvider.urlBuilder, buildGeminiGenerateContentUrl); + + const geminiAlternate = (getRegistryEntry("hcnsec")?.alternateFormats || []).find( + (a) => a.format === "gemini" + ); + assert.equal(geminiAlternate?.urlBuilder, buildGeminiGenerateContentUrl); + + assert.equal( + buildGeminiGenerateContentUrl("https://example.com/v1beta/models", "m", false), + "https://example.com/v1beta/models/m:generateContent" + ); + assert.equal( + buildGeminiGenerateContentUrl("https://example.com/v1beta/models", "m", true), + "https://example.com/v1beta/models/m:streamGenerateContent?alt=sse" + ); +}); + +test("the native gemini provider still resolves its own generateContent route", () => { + const executor = new DefaultExecutor("gemini"); + assert.equal( + executor.buildUrl("gemini-3.7-flash", false, 0, KEY), + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.7-flash:generateContent" + ); + assert.equal( + executor.buildUrl("gemini-3.7-flash", true, 0, KEY), + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.7-flash:streamGenerateContent?alt=sse" + ); +}); + +// ── dashboard / catalog registration ────────────────────────────────────────── + +test("both hosts surface in the dashboard alternate-protocol picker", () => { + assert.deepEqual( + getAlternateFormats("tabitoken").map((a) => a.format), + ["openai"] + ); + assert.deepEqual( + getAlternateFormats("hcnsec").map((a) => a.format), + ["claude", "openai-responses", "gemini"] + ); + // Every alternate needs a label — it is the string the picker renders. + for (const id of ["tabitoken", "hcnsec"]) { + for (const alternate of getAlternateFormats(id)) { + assert.ok(alternate.label, `${id}/${alternate.format} must declare a label`); + } + } +}); + +test("tabitoken is catalogued as an aggregator gateway with a display endpoint", () => { + const provider = (AI_PROVIDERS as Record>).tabitoken; + assert.ok(provider, "tabitoken must be present in AI_PROVIDERS"); + assert.equal(provider.id, "tabitoken"); + assert.equal(provider.alias, "tabitoken"); + assert.equal(provider.passthroughModels, true); + assert.equal(typeof provider.name, "string"); + assert.equal(typeof provider.website, "string"); + assert.equal(typeof provider.apiHint, "string"); + assert.ok(AGGREGATOR_PROVIDER_IDS.has("tabitoken"), "tabitoken must be an aggregator"); + // The display endpoint must name the protocol the gateway defaults to. + assert.equal( + (PROVIDER_ENDPOINTS as Record).tabitoken, + "https://tabitoken.com/v1/messages" + ); +}); + +test("extending hcnsec's protocols leaves its existing catalog classification alone", () => { + // hcnsec shipped before this change as an API-key **regional** provider (its own guard: + // tests/unit/hcnsec-provider.test.ts). Declaring three more protocols on the registry entry + // is an engine-level capability change — it must not silently reclassify the catalog entry + // as a gateway/aggregator, which would move it in the dashboard and in the generated + // provider reference. This test is the guard against that drift. + const provider = (AI_PROVIDERS as Record>).hcnsec; + assert.ok(provider, "hcnsec must remain present in AI_PROVIDERS"); + assert.equal(provider.id, "hcnsec"); + assert.equal(provider.alias, "hcnsec"); + assert.equal(provider.passthroughModels, true); + assert.equal(provider.name, "Huancheng Public API"); + assert.equal(provider.website, "https://api.hcnsec.cn"); + assert.equal(typeof provider.authHint, "string"); + assert.equal( + AGGREGATOR_PROVIDER_IDS.has("hcnsec"), + false, + "hcnsec stays a regional provider — the protocol extension must not reclassify it" + ); + + const regional = APIKEY_PROVIDERS_REGIONAL as Record; + assert.ok(regional.hcnsec, "hcnsec must stay in the regional family file"); + const gateways = APIKEY_PROVIDERS_GATEWAYS as Record; + assert.equal( + gateways.hcnsec, + undefined, + "hcnsec must not be duplicated into gateways — the families are a strict partition" + ); +}); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index ef4cf9826a..abc46e6870 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -23,7 +23,7 @@ // gateways family to 228 measured on the tip; Puter retired (#10210) and chatanywhere restored // (base-reds round 3, #9985) are both included in that measurement; Cursor API (specialty-media, // #10729) brings it to 229; Token Kiosk (gateways, #10722) — merged in the same -// merge-train batch — independently bumped the gateways family too, landing at 230. +// merge-train batch — independently bumped the gateways family too, landing at 231. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -52,12 +52,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 230 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 230); - assert.equal(new Set(keys).size, 230, "duplicate keys after spread-merge"); + assert.equal(keys.length, 231); + assert.equal(new Set(keys).size, 231, "duplicate keys after spread-merge"); // the merged object's entry-count equals the sum of the 6 semantic family files; families are a - // strict partition (every provider in exactly one), so the sum must be exactly 230. + // strict partition (every provider in exactly one), so the sum must be exactly 231. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -77,7 +77,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 230 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 230, "families must partition all 230 providers"); + assert.equal(famTotal, 231, "families must partition all 231 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => { From 0a1f1d42ee312b72f8ae7dbbde70e6ea5b64da62 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:27:26 -0300 Subject: [PATCH 118/135] fix(open-sse): declare Ollama Cloud reasoning models' supportedThinkingEfforts (#10788) glm-5.1, glm-5.2, deepseek-v4-pro and deepseek-v4-flash declared supportsReasoning:true but no supportedThinkingEfforts, so the catalog's appendSyncedEffortVariants() pass (which only synthesizes -low/-high/-max ids from an already-populated capabilities.effort_tiers) never exposed a selectable effort tier for them, unlike gpt-oss:20b/120b. Add the documented low/medium/high/max vocabulary (see supportsMaxEffortForProvider's isOllamaCloud comment in reasoningEffort.ts). --- .../fixes/10788-ollama-cloud-effort-tiers.md | 1 + .../providers/registry/ollama-cloud/index.ts | 22 +++++++++- ...cloud-reasoning-effort-tiers-10788.test.ts | 40 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10788-ollama-cloud-effort-tiers.md create mode 100644 tests/unit/ollama-cloud-reasoning-effort-tiers-10788.test.ts diff --git a/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md b/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md new file mode 100644 index 0000000000..0437576d38 --- /dev/null +++ b/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md @@ -0,0 +1 @@ +- **fix(open-sse):** declare `supportedThinkingEfforts` (`low`/`medium`/`high`/`max`) on Ollama Cloud's `glm-5.1`, `glm-5.2`, `deepseek-v4-pro` and `deepseek-v4-flash` registry entries so the catalog's `appendSyncedEffortVariants()` pass — which only synthesizes selectable `-low`/`-high`/`-max` model ids from an already-populated `capabilities.effort_tiers` — can expose an effort selector for these reasoning-capable models, matching what `gpt-oss:20b`/`gpt-oss:120b` already had (#10788) diff --git a/open-sse/config/providers/registry/ollama-cloud/index.ts b/open-sse/config/providers/registry/ollama-cloud/index.ts index 05b28df65f..4cf020263a 100644 --- a/open-sse/config/providers/registry/ollama-cloud/index.ts +++ b/open-sse/config/providers/registry/ollama-cloud/index.ts @@ -24,8 +24,24 @@ export const ollama_cloudProvider: RegistryEntry = { supportsReasoning: true, supportedThinkingEfforts: ["low", "medium", "high"], }, - { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true }, - { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true }, + // #10788: Ollama Cloud accepts low|medium|high|max|none uniformly across + // its reasoning-capable models (see supportsMaxEffortForProvider's + // isOllamaCloud comment in open-sse/executors/base/reasoningEffort.ts) — + // declare supportedThinkingEfforts so appendSyncedEffortVariants() (which + // runs before static-model capability enrichment) can synthesize the + // catalog's selectable -low/-high/-max variant ids for these models. + { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "max"], + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "max"], + }, { id: "kimi-k2.6", name: "Kimi K2.6" }, // Ollama Cloud accepts low|medium|high|max|none and rejects xhigh, so the // explicit supportsXHighEffort:false makes the sanitizer map xhigh → max. @@ -34,12 +50,14 @@ export const ollama_cloudProvider: RegistryEntry = { name: "GLM 5.1", supportsReasoning: true, supportsXHighEffort: false, + supportedThinkingEfforts: ["low", "medium", "high", "max"], }, { id: "glm-5.2", name: "GLM 5.2", supportsReasoning: true, supportsXHighEffort: false, + supportedThinkingEfforts: ["low", "medium", "high", "max"], }, // #3110: MiniMax M3 via Ollama { id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true }, diff --git a/tests/unit/ollama-cloud-reasoning-effort-tiers-10788.test.ts b/tests/unit/ollama-cloud-reasoning-effort-tiers-10788.test.ts new file mode 100644 index 0000000000..ae7506cbf1 --- /dev/null +++ b/tests/unit/ollama-cloud-reasoning-effort-tiers-10788.test.ts @@ -0,0 +1,40 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { ollama_cloudProvider } from "../../open-sse/config/providers/registry/ollama-cloud/index.ts"; + +// #10788: ollama-cloud declared supportsReasoning:true on several models +// (glm-5.1/5.2, deepseek-v4-pro/flash) but never declared +// supportedThinkingEfforts. appendSyncedEffortVariants() (open-sse/utils/ +// syncedEffortVariants.ts) only synthesizes catalog `-` ids from +// an already-populated capabilities.effort_tiers, and for static registry +// models that population only happens from a non-empty +// supportedThinkingEfforts — so these models never got a selectable +// -low/-high/-max catalog id. gpt-oss:20b/120b already declared it as the +// control case. +test("#10788: ollama-cloud reasoning-capable models declare supportedThinkingEfforts", () => { + const byId = new Map(ollama_cloudProvider.models.map((m) => [m.id, m])); + + const control = byId.get("gpt-oss:20b"); + assert.ok( + Array.isArray(control?.supportedThinkingEfforts) && control.supportedThinkingEfforts.length > 0, + "control: gpt-oss:20b should already declare supportedThinkingEfforts" + ); + + const reasoningModelIds = ["glm-5.1", "glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash"]; + for (const id of reasoningModelIds) { + const model = byId.get(id); + assert.ok(model?.supportsReasoning, `${id} should be flagged as a reasoning model`); + assert.ok( + Array.isArray(model?.supportedThinkingEfforts) && model.supportedThinkingEfforts.length > 0, + `${id} supports reasoning but declares no supportedThinkingEfforts` + ); + // Ollama Cloud's documented vocabulary (see supportsMaxEffortForProvider's + // isOllamaCloud comment in open-sse/executors/base/reasoningEffort.ts): + // low|medium|high|max|none — xhigh is rejected and mapped to max. + assert.deepEqual( + [...(model?.supportedThinkingEfforts ?? [])], + ["low", "medium", "high", "max"], + `${id} should declare Ollama Cloud's documented low/medium/high/max vocabulary` + ); + } +}); From 87719f238142f70752c32c9ab94d1e2b6d409493 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:29:38 -0300 Subject: [PATCH 119/135] fix(domain): treat unreported Antigravity quota fraction as unknown, not exhausted (#10095) --- ...ity-multiaccount-quota-false-exhaustion.md | 1 + src/domain/quotaCache.ts | 28 ++++- ...ntigravity-fraction-reported-10095.test.ts | 107 ++++++++++++++++++ 3 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md create mode 100644 tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts diff --git a/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md new file mode 100644 index 0000000000..579005e943 --- /dev/null +++ b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md @@ -0,0 +1 @@ +- fix(domain): stop treating an unreported Antigravity quota fraction (`fractionReported:false`) as 0% remaining in `quotaCache.ts`, which was falsely marking every fresh/newly-connected account as exhausted and blocking multi-account rotation (#10095) diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index 908411cf3f..1fae2cb08d 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -44,6 +44,13 @@ import { getAntigravityQuotaFamily } from "@omniroute/open-sse/services/antigrav interface QuotaInfo { remainingPercentage: number; resetAt: string | null; + // #10095 — upstream explicitly told us it did NOT report this window's + // fraction (e.g. a fresh Antigravity account or a newly-launched + // -tiered model id Google hasn't wired quota telemetry for yet). + // `undefined`/`true` means the value is a real, upstream-reported + // percentage; `false` means "unknown", so callers must not treat the + // defaulted-to-0 `remainingPercentage` as genuine exhaustion. + fractionReported?: boolean; } interface QuotaCacheEntry { @@ -113,7 +120,10 @@ const MAX_CONCURRENT_REFRESHES = 5; function isExhausted(quotas: Record): boolean { const entries = Object.values(quotas); if (entries.length === 0) return false; - return entries.every((q) => q.remainingPercentage <= 0); + // #10095 — a window whose fraction was never reported by upstream must + // never single-handedly flip the whole connection to exhausted; treat it + // as available (mirrors the guard in genericQuotaFetcher.ts). + return entries.every((q) => q.fractionReported !== false && q.remainingPercentage <= 0); } /** @@ -237,6 +247,9 @@ function normalizeQuotas(rawQuotas: Record): Record 0 ? Math.round(((q.total - (q.used || 0)) / q.total) * 100) : 0), resetAt: q.resetAt || null, + // #10095 — thread through the "did upstream actually report this + // window's fraction" signal (see UsageQuota in usage/quota.ts). + fractionReported: q.fractionReported === false ? false : undefined, }; } } @@ -641,11 +654,14 @@ export function getQuotaWindowStatus( usedPercentage, resetAt, // If reset time has already passed, avoid stale cached percentages blocking selection. - reachedThreshold: windowExpired - ? false - : remainingPercentage <= 0 - ? true - : usedPercentage >= thresholdPercent, + // #10095 — a window whose fraction upstream never reported is "unknown", + // not "0% remaining"; never let it reach the exhaustion threshold. + reachedThreshold: + windowExpired || window.fractionReported === false + ? false + : remainingPercentage <= 0 + ? true + : usedPercentage >= thresholdPercent, }; } diff --git a/tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts b/tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts new file mode 100644 index 0000000000..3cffea7ec4 --- /dev/null +++ b/tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts @@ -0,0 +1,107 @@ +// #10095 — Antigravity multi-account "all exhausted" false positive. +// +// src/domain/quotaCache.ts is the FIRST, unconditional gate every chat request +// passes through (src/sse/services/auth.ts::getProviderCredentialsWithQuotaPreflight). +// When Google's Cloud Code API doesn't report `remainingFraction` for a model +// (fresh accounts, newly-launched -tiered model ids), open-sse/services/usage/ +// antigravity.ts writes `fractionReported:false` but defaults +// `remainingPercentage` to 0 — quotaCache.ts previously read only the numeric +// percentage and treated that as genuine 0%-remaining exhaustion, so every +// freshly-connected Antigravity account looked simultaneously (and falsely) +// dead, and getProviderCredentials returned "All antigravity accounts have +// exhausted their quota" before ever trying one. +import { test, describe, before, after } from "node:test"; +import assert from "node:assert/strict"; + +describe("#10095 — quotaCache respects Antigravity fractionReported:false", () => { + before(async () => { + const { __clearForTests } = await import("../../src/domain/quotaCache.ts"); + __clearForTests(); + }); + after(async () => { + const { __clearForTests } = await import("../../src/domain/quotaCache.ts"); + __clearForTests(); + }); + + test("unreported quota window (fractionReported:false) is NOT treated as exhausted", async () => { + const { setQuotaCache, isQuotaExhaustedForRequest } = await import( + "../../src/domain/quotaCache.ts" + ); + const connectionId = "10095-fresh-account"; + // Exact shape open-sse/services/usage/antigravity.ts:660-694 writes when + // Google's API omits remainingFraction for this model. + setQuotaCache(connectionId, "antigravity", { + "gemini-3.7-flash-tiered": { + used: 0, + total: 1000, + resetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + remainingPercentage: 0, + unlimited: false, + fractionReported: false, + quotaSource: "fetchAvailableModels", + }, + }); + const exhausted = isQuotaExhaustedForRequest( + connectionId, + "antigravity", + "agy/gemini-3.7-flash-tiered" + ); + assert.equal(exhausted, false, "must NOT treat an unreported quota window as exhausted"); + }); + + test("companion: a REAL 0% window (fractionReported:true) still reports exhausted", async () => { + const { setQuotaCache, isQuotaExhaustedForRequest } = await import( + "../../src/domain/quotaCache.ts" + ); + const connectionId = "10095-genuinely-exhausted-account"; + setQuotaCache(connectionId, "antigravity", { + "gemini-3.7-flash-tiered": { + used: 1000, + total: 1000, + resetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + remainingPercentage: 0, + unlimited: false, + fractionReported: true, + quotaSource: "retrieveUserQuota", + }, + }); + const exhausted = isQuotaExhaustedForRequest( + connectionId, + "antigravity", + "agy/gemini-3.7-flash-tiered" + ); + assert.equal( + exhausted, + true, + "the fix must not blanket-disable exhaustion detection — a genuinely reported 0% window still exhausts" + ); + }); + + test("issue follow-up model id shape (agy/gemini-3.7-flash-tiered) still resolves its family", async () => { + const { setQuotaCache, isQuotaExhaustedForRequest } = await import( + "../../src/domain/quotaCache.ts" + ); + const connectionId = "10095-agy-tiered-family"; + setQuotaCache(connectionId, "agy", { + "agy/gemini-3.7-flash-tiered": { + used: 500, + total: 1000, + resetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + remainingPercentage: 50, + unlimited: false, + fractionReported: true, + quotaSource: "retrieveUserQuota", + }, + }); + const exhausted = isQuotaExhaustedForRequest( + connectionId, + "agy", + "agy/gemini-3.7-flash-tiered" + ); + assert.equal( + exhausted, + false, + "non-regression: family resolution for the agy/gemini-3.7-flash-tiered id must keep working" + ); + }); +}); From 018badc3b38684351ba02de64846c5016cdc04fe Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:30:40 -0300 Subject: [PATCH 120/135] fix: route Playground ChatTab Send to the selected endpoint, not just chat.completions (#10592) --- ...592-playground-chattab-endpoint-routing.md | 1 + .../playground/components/tabs/ChatTab.tsx | 33 ++++++- .../components/tabs/chatTabEndpointRequest.ts | 59 +++++++++++ ...nd-chat-tab-search-endpoint-10592.test.tsx | 98 +++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10592-playground-chattab-endpoint-routing.md create mode 100644 src/app/(dashboard)/dashboard/playground/components/tabs/chatTabEndpointRequest.ts create mode 100644 tests/unit/ui/playground-chat-tab-search-endpoint-10592.test.tsx diff --git a/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md new file mode 100644 index 0000000000..ca602b9122 --- /dev/null +++ b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md @@ -0,0 +1 @@ +- fix(dashboard): route the Playground's ChatTab "Send" through the endpoint actually selected in StudioConfigPane (`search`, `web.fetch`, etc.) instead of always POSTing to `/api/v1/chat/completions`, fixing the false "No active credentials for provider" 404 when testing search-only providers (#10592) diff --git a/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx b/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx index 8f048ee02d..565e667cf1 100644 --- a/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx @@ -11,6 +11,13 @@ import { getModelPricing } from "@/lib/playground/types"; import type { ConfigState } from "../StudioConfigPane"; import type { StreamMetrics } from "@/shared/schemas/playground"; import { buildReasoningRequestFields } from "../reasoningControlUtils"; +import { + buildNonChatRequestBody, + formatNonChatResponse, + isChatCompletionsEndpoint, + lastUserContent, + resolveChatTabRequestPath, +} from "./chatTabEndpointRequest"; interface Message { role: "system" | "user" | "assistant"; @@ -127,11 +134,19 @@ export default function ChatTab({ configState, onMetricsUpdate }: ChatTabProps) try { const fetchHeaders: Record = { "Content-Type": "application/json" }; + const chatEndpoint = isChatCompletionsEndpoint(configState.endpoint); + const requestBody = chatEndpoint + ? buildRequestBody(chatMessages) + : buildNonChatRequestBody( + configState.endpoint, + lastUserContent(chatMessages), + configState.model + ); - const res = await fetch("/api/v1/chat/completions", { + const res = await fetch(resolveChatTabRequestPath(configState.endpoint), { method: "POST", headers: fetchHeaders, - body: JSON.stringify(buildRequestBody(chatMessages)), + body: JSON.stringify(requestBody), signal: controller.signal, }); @@ -150,6 +165,20 @@ export default function ChatTab({ configState, onMetricsUpdate }: ChatTabProps) return; } + if (!chatEndpoint) { + const rawText = await res.text(); + setMessages((prev) => { + const next = [...prev]; + const idx = appendIndex !== undefined ? appendIndex : next.length - 1; + next[idx] = { ...next[idx], content: formatNonChatResponse(rawText) }; + return next; + }); + setResponseDuration(Date.now() - startTime); + setLoading(false); + streamMetrics.reset(); + return; + } + let firstChunk = true; const reader = res.body?.getReader(); const decoder = new TextDecoder(); diff --git a/src/app/(dashboard)/dashboard/playground/components/tabs/chatTabEndpointRequest.ts b/src/app/(dashboard)/dashboard/playground/components/tabs/chatTabEndpointRequest.ts new file mode 100644 index 0000000000..dda2b7825a --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/components/tabs/chatTabEndpointRequest.ts @@ -0,0 +1,59 @@ +// src/app/(dashboard)/dashboard/playground/components/tabs/chatTabEndpointRequest.ts +// +// #10592 — ChatTab.tsx hardcoded every "Send" click to POST /api/v1/chat/completions, +// ignoring configState.endpoint entirely. Selecting a search-only provider (exa-search, +// tavily-search, serper-search) in the Endpoint selector still sent a chat.completions +// request, which has no notion of search-provider credentials and 404s. +// +// This module gives ChatTab a small, testable seam for routing non-chat endpoints +// (currently "search" and "web.fetch") to their real path with a query-shaped body, +// instead of the chat.completions messages/SSE shape. + +import { endpointToPath, type PlaygroundEndpoint } from "@/lib/playground/codeExport"; + +/** Chat-shaped endpoints keep the existing messages[] + SSE-delta request/response flow. */ +export function isChatCompletionsEndpoint(endpoint: PlaygroundEndpoint | undefined): boolean { + return !endpoint || endpoint === "chat.completions"; +} + +/** Resolves the fetch path (mounted under `/api`) for the selected Playground endpoint. */ +export function resolveChatTabRequestPath(endpoint: PlaygroundEndpoint | undefined): string { + return `/api${endpointToPath(endpoint ?? "chat.completions")}`; +} + +/** + * Builds the request body for a non-chat endpoint from the user's free-text query. + * "search" and "web.fetch" both take a single string field instead of a messages array. + */ +export function buildNonChatRequestBody( + endpoint: PlaygroundEndpoint | undefined, + query: string, + model: string +): Record { + if (endpoint === "web.fetch") { + return { url: query }; + } + const body: Record = { query }; + if (model) body.model = model; + return body; +} + +/** Renders a non-chat endpoint's raw response text as a chat-bubble-friendly string. */ +export function formatNonChatResponse(rawText: string): string { + try { + const parsed = JSON.parse(rawText) as unknown; + return "```json\n" + JSON.stringify(parsed, null, 2) + "\n```"; + } catch { + return rawText; + } +} + +/** Finds the most recent user-authored message content to use as a non-chat query. */ +export function lastUserContent( + chatMessages: Array<{ role: string; content: string }> +): string { + for (let i = chatMessages.length - 1; i >= 0; i--) { + if (chatMessages[i].role === "user") return chatMessages[i].content; + } + return ""; +} diff --git a/tests/unit/ui/playground-chat-tab-search-endpoint-10592.test.tsx b/tests/unit/ui/playground-chat-tab-search-endpoint-10592.test.tsx new file mode 100644 index 0000000000..e38272967d --- /dev/null +++ b/tests/unit/ui/playground-chat-tab-search-endpoint-10592.test.tsx @@ -0,0 +1,98 @@ +// @vitest-environment jsdom +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/playground/types", () => ({ getModelPricing: () => null })); +vi.mock("@/lib/playground/streamMetrics", () => ({ + computeMetrics: () => ({ ttftMs: 100, totalMs: 500, tokensIn: 10, tokensOut: 20, tps: 40, costUsd: 0.001 }), +})); +vi.mock("remark-gfm", () => ({ default: () => {} })); +vi.mock("react-markdown", () => ({ + default: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); +if (typeof Element.prototype.scrollIntoView === "undefined") { + Object.defineProperty(Element.prototype, "scrollIntoView", { value: () => {}, writable: true, configurable: true }); +} +function setInputValue(el: HTMLTextAreaElement | HTMLInputElement, value: string): void { + const nativeSetter = + el instanceof HTMLTextAreaElement + ? Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set + : Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set; + nativeSetter?.call(el, value); + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); +} +const { DEFAULT_PARAMS } = await import("../../../src/app/(dashboard)/dashboard/playground/components/ParamSliders"); +const { default: ChatTab } = await import("../../../src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab"); +function makeSearchProviderConfig() { + return { + endpoint: "search" as const, + baseUrl: "http://localhost:20128", + model: "exa-search/web", + provider: "exa-search", + systemPrompt: "", + params: { ...DEFAULT_PARAMS }, + }; +} +const containers: Array<{ root: ReturnType; el: HTMLDivElement }> = []; +function renderChatTab(config: ReturnType): HTMLDivElement { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + act(() => { + root.render(); + }); + containers.push({ root, el }); + return el; +} +async function waitFor(fn: () => boolean, timeout = 3000): Promise { + const start = Date.now(); + while (!fn()) { + if (Date.now() - start > timeout) throw new Error("waitFor timed out"); + await new Promise((r) => setTimeout(r, 20)); + } +} +describe("ChatTab — search-provider endpoint routing (#10592)", () => { + beforeEach(() => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + }); + afterEach(() => { + for (const { root, el } of containers.splice(0)) { + act(() => root.unmount()); + el.remove(); + } + document.body.innerHTML = ""; + vi.restoreAllMocks(); + }); + it("routes to /api/v1/search (not /api/v1/chat/completions) when configState.endpoint is 'search'", async () => { + let capturedUrl: string | null = null; + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => { + capturedUrl = String(url); + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } } + ); + }); + const el = renderChatTab(makeSearchProviderConfig()); + const textarea = el.querySelector("textarea") as HTMLTextAreaElement; + act(() => { + setInputValue(textarea, "latest news India"); + }); + const sendBtn = Array.from(el.querySelectorAll("button")).find((b) => + b.textContent?.includes("Send") + ) as HTMLButtonElement | undefined; + await act(async () => { + sendBtn?.click(); + }); + await waitFor(() => capturedUrl !== null); + expect(capturedUrl).toBe("/api/v1/search"); + fetchSpy.mockRestore(); + }); +}); From bed4d24049982550f892e5c899015e21dd8d8c64 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:33:19 -0300 Subject: [PATCH 121/135] fix(config): exclude cookie-auth image bridges from unprefixed model scan (#10848) --- .../fixes/10848-image-scan-cookie-bridge.md | 1 + open-sse/config/imageRegistry.ts | 4 +-- .../unprefixed-scan-web-cookie-10848.test.ts | 34 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10848-image-scan-cookie-bridge.md create mode 100644 tests/unit/unprefixed-scan-web-cookie-10848.test.ts diff --git a/changelog.d/fixes/10848-image-scan-cookie-bridge.md b/changelog.d/fixes/10848-image-scan-cookie-bridge.md new file mode 100644 index 0000000000..07e0f20291 --- /dev/null +++ b/changelog.d/fixes/10848-image-scan-cookie-bridge.md @@ -0,0 +1 @@ +- fix(config): exclude cookie-auth image bridges (chatgpt-web, gemini-web) from the unprefixed model scan so a bare id never silently binds to an unofficial web bridge (#10848) diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 8defd6d8a8..650dcdd69c 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -918,9 +918,9 @@ export function parseImageModel(modelStr) { } } - // No provider prefix — try to find the model in every provider + // No provider prefix — try to find the model in every provider, excluding cookie-auth (web) bridges for (const [providerId, config] of Object.entries(IMAGE_PROVIDERS)) { - if (config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr)) { + if (config.authHeader !== "cookie" && (config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr))) { return { provider: providerId, model: modelStr }; } } diff --git a/tests/unit/unprefixed-scan-web-cookie-10848.test.ts b/tests/unit/unprefixed-scan-web-cookie-10848.test.ts new file mode 100644 index 0000000000..1d5683401c --- /dev/null +++ b/tests/unit/unprefixed-scan-web-cookie-10848.test.ts @@ -0,0 +1,34 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { IMAGE_PROVIDERS, parseImageModel } from "../../open-sse/config/imageRegistry.ts"; + +test("#10848 bare id that only exists on a cookie-auth web bridge should not silently resolve to it", () => { + const chatgptWeb = IMAGE_PROVIDERS["chatgpt-web"]; + assert.equal(chatgptWeb.authHeader, "cookie"); + const otherProvidersWithSameId = Object.entries(IMAGE_PROVIDERS).filter( + ([providerId, config]) => + providerId !== "chatgpt-web" && config.models.some((m) => m.id === "gpt-5.5") + ); + assert.deepEqual( + otherProvidersWithSameId, + [], + "expected only chatgpt-web (cookie) to register gpt-5.5" + ); + + const resolved = parseImageModel("gpt-5.5"); + + assert.notDeepEqual( + resolved, + { provider: "chatgpt-web", model: "gpt-5.5" }, + "bare 'gpt-5.5' must not silently bind to the cookie-auth chatgpt-web bridge" + ); + + assert.deepEqual(parseImageModel("chatgpt-web/gpt-5.5"), { + provider: "chatgpt-web", + model: "gpt-5.5", + }); + assert.deepEqual(parseImageModel("cgpt-web/gpt-5.5"), { + provider: "chatgpt-web", + model: "gpt-5.5", + }); +}); From 6d043674c2dae3b09c0c6ad58ef9aa4306ad9025 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:34:19 -0300 Subject: [PATCH 122/135] fix: skip expensive RTK compression stats computation on no-op runs (#10765) --- .../10765-rtk-unconditional-stats-cpu.md | 1 + .../services/compression/engines/rtk/index.ts | 12 +++++++ tests/unit/compression/rtk-engine.test.ts | 3 +- tests/unit/probe-10765-rtk-noop-stats.test.ts | 32 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md create mode 100644 tests/unit/probe-10765-rtk-noop-stats.test.ts diff --git a/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md new file mode 100644 index 0000000000..ff36462d47 --- /dev/null +++ b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md @@ -0,0 +1 @@ +- fix(compression): skip the expensive `createCompressionStats()` pass in RTK when no message was actually compressed, matching every sibling stacked engine (#10765) diff --git a/open-sse/services/compression/engines/rtk/index.ts b/open-sse/services/compression/engines/rtk/index.ts index 33b6c90b59..b3791bfc54 100644 --- a/open-sse/services/compression/engines/rtk/index.ts +++ b/open-sse/services/compression/engines/rtk/index.ts @@ -656,6 +656,18 @@ export function applyRtkCompression( }; }); + // Mirror the sibling stacked engines (headroom, session-dedup, ccr, relevance, + // ionizer, readLifecycle): skip the expensive createCompressionStats() pass + // (full JSON.stringify + tokenizer over the whole body, twice) when nothing + // actually changed. Untouched messages keep their original reference above, + // so a reference-identity scan is enough to detect the no-op case (#10765). + const anyMessageChanged = compressedMessages.some( + (message, index) => message !== messages[index] + ); + if (!anyMessageChanged) { + return { body, compressed: false, stats: null }; + } + const compressedBody = { ...adapter.body, messages: compressedMessages }; const stats = createCompressionStats( adapter.body, diff --git a/tests/unit/compression/rtk-engine.test.ts b/tests/unit/compression/rtk-engine.test.ts index 7406f53634..4741ddb6e6 100644 --- a/tests/unit/compression/rtk-engine.test.ts +++ b/tests/unit/compression/rtk-engine.test.ts @@ -70,7 +70,8 @@ describe("RTK compression engine", () => { assert.equal(rtkEngine.validateConfig({ intensity: "invalid" }).valid, false); assert.equal(rtkEngine.validateConfig({ rawOutputRetention: "always" }).valid, true); - const body = { messages: [{ role: "tool", content: "same\nsame\nsame\nsame" }] }; + const repeated = Array.from({ length: 20 }, () => "same").join("\n"); + const body = { messages: [{ role: "tool", content: repeated }] }; assert.equal( rtkEngine.apply(body, { config: { rtkConfig: { enabled: true } } }).stats?.engine, "rtk" diff --git a/tests/unit/probe-10765-rtk-noop-stats.test.ts b/tests/unit/probe-10765-rtk-noop-stats.test.ts new file mode 100644 index 0000000000..2826af2a71 --- /dev/null +++ b/tests/unit/probe-10765-rtk-noop-stats.test.ts @@ -0,0 +1,32 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { applyRtkCompression } from "../../open-sse/services/compression/engines/rtk/index.ts"; + +// Issue #10765: enabling RTK causes ~100% CPU even when the engine finds nothing to +// compress ("It also occurs when the engine does not modify the request and reports +// no token savings."). Root cause: applyRtkCompression() unconditionally calls +// createCompressionStats() at the end of the function — which does a full +// JSON.stringify() (+ tiktoken tokenize for Codex bodies) of the ENTIRE request body, +// TWICE (original + compressed) — even when zero messages were touched. +// +// Every sibling stacked engine (headroom, session-dedup, ccr, relevance, ionizer, +// readLifecycle) returns `stats: null` early when nothing changed, skipping this +// expensive computation entirely. RTK is the outlier: it always pays the cost. +test("RTK no-op run should skip the expensive stats computation (like sibling engines)", () => { + const body = { + model: "codex/gpt-5", + provider: "codex", + messages: [ + { role: "user", content: "hello, this is a simple message with nothing to compress" }, + ], + }; + + const result = applyRtkCompression(body, { config: { enabled: true } }); + + assert.equal(result.compressed, false, "RTK made no changes"); + assert.equal( + result.stats, + null, + "RTK should return stats: null on a no-op run, like every sibling stacked engine" + ); +}); From 9603ec1bf1e43c4f5ebd7abf8e4e762299490c60 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:34:38 -0300 Subject: [PATCH 123/135] fix(sse): log upstream error body in COMBO per-target failure warnings (#10597) --- .../fixes/10597-combo-log-error-body.md | 1 + open-sse/services/combo.ts | 10 +- .../combo-10597-error-body-logging.test.ts | 91 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10597-combo-log-error-body.md create mode 100644 tests/unit/combo-10597-error-body-logging.test.ts diff --git a/changelog.d/fixes/10597-combo-log-error-body.md b/changelog.d/fixes/10597-combo-log-error-body.md new file mode 100644 index 0000000000..ff6608947c --- /dev/null +++ b/changelog.d/fixes/10597-combo-log-error-body.md @@ -0,0 +1 @@ +- **fix(sse):** Include the redacted upstream error body in the per-target COMBO failure log (`Model X failed, trying next`) so operators can triage a 400/500 without reproducing the request ([#10597](https://github.com/diegosouzapw/OmniRoute/issues/10597)) diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index cf602cf98d..2268c2050c 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -2275,7 +2275,10 @@ async function handleComboChatInner({ ); } } - log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status }); + log.warn("COMBO", `Model ${modelStr} failed, trying next`, { + status: result.status, + errorBody: redactConnectionLabel(errorText), + }); // #5976: per-model-quota providers (Gemini, GitHub, etc.) multiplex models // behind one connection. A model-level 500 or 429 (RPM) must NOT cool down @@ -3460,7 +3463,10 @@ async function handleRoundRobinCombo({ kind: classifyComboOutcome(result.status, errorText), }); if (offset > 0) fallbackCount++; - log.warn("COMBO-RR", `${modelStr} failed, trying next model`, { status: result.status }); + log.warn("COMBO-RR", `${modelStr} failed, trying next model`, { + status: result.status, + errorBody: redactConnectionLabel(errorText), + }); if ( resilienceSettings.providerCooldown.enabled && diff --git a/tests/unit/combo-10597-error-body-logging.test.ts b/tests/unit/combo-10597-error-body-logging.test.ts new file mode 100644 index 0000000000..6df6a2cf49 --- /dev/null +++ b/tests/unit/combo-10597-error-body-logging.test.ts @@ -0,0 +1,91 @@ +/** + * #10597 — When a combo target fails with a non-2xx status, the per-target + * "Model X failed, trying next" COMBO log line only carries `{ status }` — + * the upstream error BODY (e.g. Anthropic's "prompt is too long" or a + * tool_use/tool_result pairing 400) is captured in `errorText` but never + * logged, so operators cannot distinguish failure causes from server logs + * without reproducing the request. + */ +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-combo-10597-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-10597-test-secret"; + +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); + +const DISTINCTIVE_ERROR_TEXT = + "messages.450: `tool_use` ids were found without `tool_result` blocks immediately after"; + +type WarnCall = { tag: string; msg: string; meta: unknown }; +const warnCalls: WarnCall[] = []; +const log = { + info: () => {}, + debug: () => {}, + error: () => {}, + warn: (tag: string, msg: string, meta?: unknown) => { + warnCalls.push({ tag, msg, meta }); + }, +}; + +function failing400() { + return new Response( + JSON.stringify({ + type: "error", + error: { type: "invalid_request_error", message: DISTINCTIVE_ERROR_TEXT }, + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); +} + +function healthy200(model: string) { + return new Response( + JSON.stringify({ + id: "ok", + object: "chat.completion", + model, + choices: [{ index: 0, message: { role: "assistant", content: "hello from " + model }, finish_reason: "stop" }], + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); +} + +function makeCombo(models: string[]) { + return { name: "test-combo-10597", strategy: "priority", models: models.map((m) => ({ model: m })) }; +} + +test("#10597 COMBO failure log must surface the upstream error body, not just the status code", async () => { + const modelsCalled: string[] = []; + const handleSingleModel = async (_body: unknown, modelStr: string) => { + modelsCalled.push(modelStr); + if (modelsCalled.length === 1) return failing400(); + return healthy200(modelStr); + }; + + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "hi" }] }, + combo: makeCombo(["claude/claude-opus-4-8", "openai/gpt-4o-mini"]), + handleSingleModel, + log, + settings: {}, + allCombos: [], + }); + + assert.equal(result.status, 200); + assert.equal(modelsCalled.length, 2); + + const failureLog = warnCalls.find( + (c) => typeof c.msg === "string" && c.msg.includes("claude/claude-opus-4-8") && c.msg.includes("failed") + ); + assert.ok(failureLog, "expected a COMBO warn log for the failing leg"); + + const serialized = JSON.stringify(failureLog); + assert.ok( + serialized.includes("tool_use") || serialized.includes(DISTINCTIVE_ERROR_TEXT), + `expected the upstream error body to appear in the COMBO failure log, but got: ${serialized}` + ); +}); From 0458c5ac4ce8ac29f399fec51fd962e29cc8cf9b Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:34:59 -0300 Subject: [PATCH 124/135] fix(db): disambiguate Kiro OAuth dedup by profileArn (#10815) --- .../10815-kiro-oauth-profilearn-dedup.md | 1 + src/lib/db/providers.ts | 35 +++--- src/lib/db/webSessionDedup.ts | 40 ++++++ ...kiro-second-oauth-connection-10815.test.ts | 115 ++++++++++++++++++ 4 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md create mode 100644 tests/unit/kiro-second-oauth-connection-10815.test.ts diff --git a/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md new file mode 100644 index 0000000000..51768aab4c --- /dev/null +++ b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md @@ -0,0 +1 @@ +- fix(db): disambiguate `createProviderConnection()`'s OAuth email dedup by `providerSpecificData.profileArn` in addition to `username`, so adding a second Kiro/AWS profile with the same email creates a new connection instead of silently merging into the first (#10815) diff --git a/src/lib/db/providers.ts b/src/lib/db/providers.ts index 571ec2787b..3b02933e11 100644 --- a/src/lib/db/providers.ts +++ b/src/lib/db/providers.ts @@ -26,7 +26,11 @@ import { isBcryptHash, verifyManagementPassword, } from "@/lib/auth/managementPassword"; -import { webSessionCredentialKey, parseProviderSpecificData } from "./webSessionDedup"; +import { + webSessionCredentialKey, + parseProviderSpecificData, + isMatchingOauthIdentity, +} from "./webSessionDedup"; import { pickCodexConnectionForUser } from "@/lib/oauth/utils/codexConnectionSelection"; import { reconcileCodexUsageHistory } from "./providers/usageIdentityReconciliation"; @@ -435,30 +439,25 @@ export async function createProviderConnection(data: JsonRecord) { } } else { // For other providers (or Codex without workspaceId), match on email — - // disambiguated by providerSpecificData.username when present on both - // sides. Two different IdPs can share the same email address (e.g. a - // Google account and a HuggingFace account); matching on email alone - // would silently overwrite the other account's connection on the - // second login. Only fall back to the bare email-only match when - // neither side carries a username (legacy rows created before this - // disambiguation existed). + // disambiguated by providerSpecificData.username and/or + // providerSpecificData.profileArn when present on both sides. Two + // different IdPs (or two distinct Kiro/AWS profiles authenticated via + // the same email-carrying IdP) can share the same email address; + // matching on email alone would silently overwrite the other + // account's connection on the second login. Only fall back to the + // bare email-only match when neither side carries a username/profileArn + // (legacy rows created before this disambiguation existed). const incomingUsername = toStringOrNull(providerSpecificData.username); + const incomingProfileArn = toStringOrNull(providerSpecificData.profileArn); const emailMatches = db .prepare( "SELECT * FROM provider_connections WHERE provider = ? AND auth_type = 'oauth' AND email = ?" ) .all(data.provider, data.email) as JsonRecord[]; existing = - emailMatches.find((row) => { - const existingUsername = toStringOrNull( - parseProviderSpecificData(row.provider_specific_data)?.username - ); - if (incomingUsername && existingUsername) { - return incomingUsername === existingUsername; - } - if (incomingUsername || existingUsername) return false; - return true; - }) || null; + emailMatches.find((row) => + isMatchingOauthIdentity(row, incomingUsername, incomingProfileArn) + ) || null; } } else if (data.authType === "apikey") { // Name-based upsert (existing behavior): same provider + same name → update. diff --git a/src/lib/db/webSessionDedup.ts b/src/lib/db/webSessionDedup.ts index 341afc9a2d..b68ee00122 100644 --- a/src/lib/db/webSessionDedup.ts +++ b/src/lib/db/webSessionDedup.ts @@ -55,3 +55,43 @@ export function parseProviderSpecificData(raw: unknown): Record } return null; } + +/** Trimmed non-empty string, else null — local to avoid a cross-module import for one coercion. */ +function nonEmptyString(value: unknown): string | null { + return typeof value === "string" && value.trim() ? value.trim() : null; +} + +/** + * Two-sided disambiguator match: `true` when both sides agree, `false` when + * both carry a value and it differs, `undefined` when the field can't decide + * (at most one side carries it) — the caller then defers to other fields. + */ +function fieldMatch(incoming: string | null, existing: string | null): boolean | undefined { + if (incoming && existing) return incoming === existing; + if (incoming || existing) return false; + return undefined; +} + +/** + * Decide whether `row` (an existing `provider_connections` record) is the + * same OAuth identity as an incoming connection carrying `incomingUsername` + * and `incomingProfileArn` (#10815). + * + * Two independent disambiguators, either of which can prove "different + * account": `providerSpecificData.username` (Raycast-style IdP dedup) and + * `providerSpecificData.profileArn` (Kiro/AWS profile dedup — Kiro never + * sets `username`). A field only rules a match IN/OUT when both the + * incoming and existing record carry it; when neither carries either field + * the legacy bare-email match still applies unchanged. + */ +export function isMatchingOauthIdentity( + row: { provider_specific_data?: unknown }, + incomingUsername: string | null, + incomingProfileArn: string | null +): boolean { + const existingPsd = parseProviderSpecificData(row.provider_specific_data); + const usernameMatch = fieldMatch(incomingUsername, nonEmptyString(existingPsd?.username)); + const profileArnMatch = fieldMatch(incomingProfileArn, nonEmptyString(existingPsd?.profileArn)); + if (usernameMatch === false || profileArnMatch === false) return false; + return true; +} diff --git a/tests/unit/kiro-second-oauth-connection-10815.test.ts b/tests/unit/kiro-second-oauth-connection-10815.test.ts new file mode 100644 index 0000000000..4b2640d99b --- /dev/null +++ b/tests/unit/kiro-second-oauth-connection-10815.test.ts @@ -0,0 +1,115 @@ +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-kiro-10815-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("createProviderConnection keeps two Kiro oauth connections with the same email but different profileArn separate (#10815)", async () => { + const first = await providersDb.createProviderConnection({ + provider: "kiro", + authType: "oauth", + email: "user@example.com", + accessToken: "token-account-1", + refreshToken: "refresh-account-1", + providerSpecificData: { + authMethod: "imported", + provider: "Google", + profileArn: "arn:aws:codewhisperer:us-east-1:111111111111:profile/AAAA", + }, + }); + + const second = await providersDb.createProviderConnection({ + provider: "kiro", + authType: "oauth", + email: "user@example.com", + accessToken: "token-account-2", + refreshToken: "refresh-account-2", + providerSpecificData: { + authMethod: "imported", + provider: "Google", + profileArn: "arn:aws:codewhisperer:us-east-1:222222222222:profile/BBBB", + }, + }); + + const kiroConnections = await providersDb.getProviderConnections({ provider: "kiro" }); + + assert.notEqual( + second.id, + first.id, + "second Kiro connection should be a new row, not an update of the first" + ); + assert.equal( + kiroConnections.length, + 2, + `expected 2 Kiro connections after adding a second account, got ${kiroConnections.length}` + ); +}); + +test("createProviderConnection re-auth of the SAME Kiro profileArn still updates in place (#10815)", async () => { + const first = await providersDb.createProviderConnection({ + provider: "kiro", + authType: "oauth", + email: "same-profile@example.com", + accessToken: "token-a", + refreshToken: "refresh-a", + providerSpecificData: { + authMethod: "imported", + provider: "Google", + profileArn: "arn:aws:codewhisperer:us-east-1:333333333333:profile/CCCC", + }, + }); + + const reauth = await providersDb.createProviderConnection({ + provider: "kiro", + authType: "oauth", + email: "same-profile@example.com", + accessToken: "token-a-refreshed", + refreshToken: "refresh-a-refreshed", + providerSpecificData: { + authMethod: "imported", + provider: "Google", + profileArn: "arn:aws:codewhisperer:us-east-1:333333333333:profile/CCCC", + }, + }); + + assert.equal( + reauth.id, + first.id, + "re-auth of the same profileArn should update the existing row" + ); +}); + +test("createProviderConnection keeps legacy email-only OAuth dedup for rows without profileArn/username (#10815)", async () => { + const first = await providersDb.createProviderConnection({ + provider: "google", + authType: "oauth", + email: "legacy@example.com", + accessToken: "legacy-token-1", + refreshToken: "legacy-refresh-1", + }); + + const second = await providersDb.createProviderConnection({ + provider: "google", + authType: "oauth", + email: "legacy@example.com", + accessToken: "legacy-token-2", + refreshToken: "legacy-refresh-2", + }); + + assert.equal( + second.id, + first.id, + "legacy rows without profileArn/username should still dedup by bare email match" + ); +}); From 4ec080dc19fbbd224927c74d02b9a56f9d0f99af Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:37:22 -0300 Subject: [PATCH 125/135] fix(api): POST /v1/search names unknown providers instead of opaque 400 (#10849) v1SearchSchema.provider was a hard-coded z.enum that rejected any id outside its list before the route's own resolveSearchProvider() check ever ran, so unknown/short-alias provider ids (grok, brave, serper, ...) always surfaced a generic "Invalid request" instead of the informative "Unknown search provider: " message. Relax the schema to a free-form string and let resolveSearchProvider() own runtime validation (as it already did for ids that passed the enum). Also extend SEARCH_PROVIDER_ALIASES with short-form aliases mirroring the existing jina/jina-ai pattern (brave, serper, perplexity, exa, tavily, google-pse, linkup, ollama, searchapi, youcom, searxng, zai, duckduckgo), and surface the first Zod validation issue's field name instead of the generic message for other still-invalid fields (e.g. search_type). --- .../fixes/10849-search-provider-opaque-400.md | 1 + open-sse/config/searchRegistry.ts | 13 ++++ src/app/api/v1/search/route.ts | 8 ++- src/shared/validation/helpers.ts | 13 ++++ src/shared/validation/schemas/apiV1.ts | 31 +++------ tests/unit/firecrawl-search.test.ts | 21 +++++- .../search-provider-opaque-400-10849.test.ts | 69 +++++++++++++++++++ tests/unit/search-registry.test.ts | 10 ++- 8 files changed, 138 insertions(+), 28 deletions(-) create mode 100644 changelog.d/fixes/10849-search-provider-opaque-400.md create mode 100644 tests/unit/search-provider-opaque-400-10849.test.ts diff --git a/changelog.d/fixes/10849-search-provider-opaque-400.md b/changelog.d/fixes/10849-search-provider-opaque-400.md new file mode 100644 index 0000000000..a8982fb194 --- /dev/null +++ b/changelog.d/fixes/10849-search-provider-opaque-400.md @@ -0,0 +1 @@ +- fix(api): POST /v1/search now replies with a named `Unknown search provider: ` error (and field-named validation messages) instead of an opaque `Invalid request` for unrecognized or short-alias provider ids like `brave`/`serper` (#10849) diff --git a/open-sse/config/searchRegistry.ts b/open-sse/config/searchRegistry.ts index ce20777cb0..9230fa1b0e 100644 --- a/open-sse/config/searchRegistry.ts +++ b/open-sse/config/searchRegistry.ts @@ -303,6 +303,19 @@ export const SEARCH_CREDENTIAL_FALLBACKS: Record = { export const SEARCH_PROVIDER_ALIASES: Record = { "jina-ai": "jina-search", jina: "jina-search", + brave: "brave-search", + serper: "serper-search", + perplexity: "perplexity-search", + exa: "exa-search", + tavily: "tavily-search", + "google-pse": "google-pse-search", + linkup: "linkup-search", + ollama: "ollama-search", + searchapi: "searchapi-search", + youcom: "youcom-search", + searxng: "searxng-search", + zai: "zai-search", + duckduckgo: "duckduckgo-free", }; export function resolveSearchProviderId(providerId: string): string { diff --git a/src/app/api/v1/search/route.ts b/src/app/api/v1/search/route.ts index 7f9b1011aa..29f642211e 100644 --- a/src/app/api/v1/search/route.ts +++ b/src/app/api/v1/search/route.ts @@ -19,7 +19,11 @@ import * as log from "@/sse/utils/logger"; import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; import { v1SearchSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { + formatValidationMessage, + isValidationFailure, + validateBody, +} from "@/shared/validation/helpers"; import { recordCost } from "@/domain/costRules"; import { computeCacheKey, @@ -120,7 +124,7 @@ async function postHandler(request: Request, context: unknown) { const validation = validateBody(v1SearchSchema, rawBody); if (isValidationFailure(validation)) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message); + return errorResponse(HTTP_STATUS.BAD_REQUEST, formatValidationMessage(validation.error)); } const body = validation.data; diff --git a/src/shared/validation/helpers.ts b/src/shared/validation/helpers.ts index b10ca0c7bd..4e486c7287 100644 --- a/src/shared/validation/helpers.ts +++ b/src/shared/validation/helpers.ts @@ -56,6 +56,19 @@ export function isValidationFailure( return validation.success === false; } +/** + * Build a human-readable 400 message from a validation failure, naming the + * first offending field instead of the generic "Invalid request" (#10849). + * Intended for routes that reply with a single message string (e.g. + * `errorResponse()`) rather than the full `{ message, details }` envelope + * returned by `validatedJsonBody()`. + */ +export function formatValidationMessage(error: ValidationErrorPayload): string { + const [first] = error.details; + if (!first) return error.message; + return first.field ? `${first.field}: ${first.message}` : first.message; +} + /** * Result of attempting to parse and validate a JSON body against a Zod schema. * diff --git a/src/shared/validation/schemas/apiV1.ts b/src/shared/validation/schemas/apiV1.ts index ba8f61067b..35c53d3f5d 100644 --- a/src/shared/validation/schemas/apiV1.ts +++ b/src/shared/validation/schemas/apiV1.ts @@ -568,27 +568,16 @@ export const v1SearchSchema = z .trim() .min(1, "Query is required") .max(500, "Query must be 500 characters or fewer"), - provider: z - .enum([ - "serper-search", - "brave-search", - "perplexity-search", - "exa-search", - "tavily-search", - "firecrawl", - "google-pse-search", - "linkup-search", - "ollama-search", - "searchapi-search", - "youcom-search", - "searxng-search", - "zai-search", - "jina-search", - "jina-ai", - "jina", - "duckduckgo-free", - ]) - .optional(), + // Not a z.enum: the runtime catalog (SEARCH_PROVIDERS + SEARCH_PROVIDER_ALIASES in + // open-sse/config/searchRegistry.ts) is the source of truth via resolveSearchProvider(), + // which already returns a named "Unknown search provider: " error for bad ids (see + // src/app/api/v1/search/route.ts). A hard-coded enum here would 400 before that check + // ever runs, hiding the informative message behind a generic Zod failure (#10849). + // Known catalog ids as of this writing: serper-search, brave-search, perplexity-search, + // exa-search, tavily-search, firecrawl, google-pse-search, linkup-search, ollama-search, + // searchapi-search, youcom-search, searxng-search, zai-search, jina-search, jina-ai, + // jina, duckduckgo-free (plus short aliases resolved by SEARCH_PROVIDER_ALIASES). + provider: z.string().min(1).optional(), max_results: z.coerce.number().int().min(1).max(100).default(5), search_type: z.enum(["web", "news"]).default("web"), offset: z.coerce.number().int().min(0).default(0), diff --git a/tests/unit/firecrawl-search.test.ts b/tests/unit/firecrawl-search.test.ts index 601fa1a8ad..c37a6a9b99 100644 --- a/tests/unit/firecrawl-search.test.ts +++ b/tests/unit/firecrawl-search.test.ts @@ -12,8 +12,13 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { SEARCH_PROVIDERS, SEARCH_CREDENTIAL_FALLBACKS, getSearchProvider, selectProvider } = - await import("../../open-sse/config/searchRegistry.ts"); +const { + SEARCH_PROVIDERS, + SEARCH_CREDENTIAL_FALLBACKS, + getSearchProvider, + selectProvider, + resolveSearchProvider, +} = await import("../../open-sse/config/searchRegistry.ts"); const { handleSearch } = await import("../../open-sse/handlers/search.ts"); const { v1SearchSchema } = await import("../../src/shared/validation/schemas.ts"); @@ -54,8 +59,18 @@ test("v1SearchSchema accepts firecrawl for search (unified id)", () => { search_type: "news", }); assert.equal(news.success, true); + // #10849: v1SearchSchema.provider is a free-form string, not a hard-coded enum, so + // the runtime catalog (resolveSearchProvider()) is the source of truth for whether an + // id is valid — the legacy "firecrawl-search" id is still rejected, just downstream of + // the schema (route.ts replies "Unknown search provider: firecrawl-search") instead of + // by an opaque schema-level 400. const legacy = v1SearchSchema.safeParse({ query: "q", provider: "firecrawl-search" }); - assert.equal(legacy.success, false, "legacy firecrawl-search id is not accepted"); + assert.equal(legacy.success, true, "provider is a free-form string at the schema layer"); + assert.equal( + resolveSearchProvider("firecrawl-search"), + null, + "legacy firecrawl-search id does not resolve to a registered provider" + ); }); test("handleSearch firecrawl hits /v2/search with sources web and normalizes data.web", async () => { diff --git a/tests/unit/search-provider-opaque-400-10849.test.ts b/tests/unit/search-provider-opaque-400-10849.test.ts new file mode 100644 index 0000000000..431675f3bb --- /dev/null +++ b/tests/unit/search-provider-opaque-400-10849.test.ts @@ -0,0 +1,69 @@ +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-search-10849-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const searchRoute = await import("../../src/app/api/v1/search/route.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +function makeRequest(body: unknown) { + return new Request("http://localhost/v1/search", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +type ErrorBody = { error?: { message: string } }; + +test("#10849: unknown provider id returns 'Unknown search provider: ', not opaque 'Invalid request'", async () => { + const response = await searchRoute.POST(makeRequest({ query: "test", provider: "grok" }), {}); + const body = (await response.json()) as ErrorBody; + + assert.equal(response.status, 400); + assert.match( + body.error?.message ?? "", + /Unknown search provider: grok/, + `expected a named-provider message, got: ${body.error?.message}` + ); +}); + +test("#10849: short alias 'brave' resolves like existing 'jina' aliases (not an opaque 400)", async () => { + const response = await searchRoute.POST(makeRequest({ query: "test", provider: "brave" }), {}); + const body = (await response.json()) as ErrorBody; + + assert.notEqual( + body.error?.message, + "Invalid request", + `expected a named provider error, got opaque: ${JSON.stringify(body.error)}` + ); +}); + +test("#10849: a genuinely bad field surfaces a non-generic, field-named 400 message", async () => { + const response = await searchRoute.POST( + makeRequest({ query: "test", search_type: "bogus" }), + {} + ); + const body = (await response.json()) as ErrorBody; + + assert.equal(response.status, 400); + assert.notEqual( + body.error?.message, + "Invalid request", + `expected a field-named message, got opaque: ${JSON.stringify(body.error)}` + ); + assert.match( + body.error?.message ?? "", + /search_type/, + `expected the message to name the offending field, got: ${body.error?.message}` + ); +}); diff --git a/tests/unit/search-registry.test.ts b/tests/unit/search-registry.test.ts index d4b2eea735..b5df867b18 100644 --- a/tests/unit/search-registry.test.ts +++ b/tests/unit/search-registry.test.ts @@ -381,11 +381,17 @@ test("v1SearchSchema rejects query over 500 chars", async () => { assert.ok(!result.success); }); -test("v1SearchSchema rejects invalid provider", async () => { +test("v1SearchSchema accepts any non-empty provider string; the catalog rejects unknown ids (#10849)", async () => { const { v1SearchSchema } = await import("../../src/shared/validation/schemas.ts"); + const { resolveSearchProvider } = await import("../../open-sse/config/searchRegistry.ts"); + // provider is a free-form string at the schema layer — resolveSearchProvider() (backing + // POST /v1/search) is the runtime source of truth, and returns null for unknown ids so + // the route can reply with a named "Unknown search provider: " error instead of an + // opaque schema-level 400. const result = v1SearchSchema.safeParse({ query: "test", provider: "google" }); - assert.ok(!result.success); + assert.ok(result.success); + assert.equal(resolveSearchProvider("google"), null); }); test("v1SearchSchema accepts tavily provider", async () => { From b668d91364f4c12accd60e7d83b2118d3bdea41a Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:41:59 -0300 Subject: [PATCH 126/135] fix(sse): canonicalize alias provider ids before quota fetcher lookup (#10877) --- .../10877-quota-alias-fetcher-lookup-gap.md | 1 + open-sse/services/combo.ts | 6 +- open-sse/services/combo/quotaScoring.ts | 7 +- .../quota-scoring-alias-lookup-10877.test.ts | 66 +++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md create mode 100644 tests/unit/quota-scoring-alias-lookup-10877.test.ts diff --git a/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md new file mode 100644 index 0000000000..9501c6dad2 --- /dev/null +++ b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md @@ -0,0 +1 @@ +- **fix(sse):** `getResetAwareProvider()` and the auto-combo quota lookup in `combo.ts` now canonicalize the provider id via `resolveProviderId()` before calling `getQuotaFetcher()`, so a fetcher registered under a provider's canonical id (e.g. `ollama-cloud`, `codex`) is found for combo targets stored under an alias spelling (e.g. `ollamacloud`, `cx`) instead of silently degrading reset-aware/reset-window/auto quota-aware routing to plain priority ordering (#10877) diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index cf602cf98d..f9947bb71c 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -64,6 +64,7 @@ import { getHiddenModelsByProvider } from "@/models"; import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLockoutSettings"; import { fetchCodexQuota } from "./codexQuotaFetcher.ts"; import { evaluateQuotaCutoff, getQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts"; +import { resolveProviderId } from "../../src/shared/constants/providers.ts"; import * as semaphore from "./rateLimitSemaphore.ts"; import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker"; import { parseModel } from "./model.ts"; @@ -491,7 +492,10 @@ export async function buildAutoCandidates( let quotaRemaining = 100; let quotaCutoffBlocked = false; let quotaCutoffReason: string | undefined; - const fetcher = getQuotaFetcher(provider); + // #10877: `provider` here may be a legacy/user-facing alias spelling + // (target.provider/parseModel output); canonicalize before the fetcher + // registry lookup so aliased combo members still hit quota-aware scoring. + const fetcher = getQuotaFetcher(resolveProviderId(provider)); const connection = target.connectionId ? connectionById.get(target.connectionId) : undefined; const authType = typeof connection?.authType === "string" ? connection.authType : null; const sessionAvailability = diff --git a/open-sse/services/combo/quotaScoring.ts b/open-sse/services/combo/quotaScoring.ts index 4a853b1455..a107768278 100644 --- a/open-sse/services/combo/quotaScoring.ts +++ b/open-sse/services/combo/quotaScoring.ts @@ -14,6 +14,7 @@ import { isRecord } from "./comboData.ts"; import type { SlaRoutingPolicy } from "../autoCombo/routerStrategy.ts"; import { RESET_WINDOW_NAMES } from "./types.ts"; import type { ResolvedComboTarget } from "./types.ts"; +import { resolveProviderId } from "../../../src/shared/constants/providers.ts"; const RESET_AWARE_SESSION_WINDOW_MS = 5 * 60 * 60 * 1000; const RESET_AWARE_WEEKLY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; @@ -138,7 +139,11 @@ export function resolveSlaRoutingPolicy( export function getResetAwareProvider(target: ResolvedComboTarget): string | null { const provider = (target.providerId || target.provider || "").toLowerCase(); - return provider || null; + // #10877: combo targets can carry a legacy/user-facing alias spelling + // (e.g. "ollamacloud", "cx") while quota fetchers register under the + // canonical provider id (e.g. "ollama-cloud", "codex"). Canonicalize here + // so getQuotaFetcher() lookups downstream (quotaStrategies.ts) find them. + return provider ? resolveProviderId(provider) : null; } function normalizeResetAt(value: unknown): string | null { diff --git a/tests/unit/quota-scoring-alias-lookup-10877.test.ts b/tests/unit/quota-scoring-alias-lookup-10877.test.ts new file mode 100644 index 0000000000..4039ceeffc --- /dev/null +++ b/tests/unit/quota-scoring-alias-lookup-10877.test.ts @@ -0,0 +1,66 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { getResetAwareProvider } from "../../open-sse/services/combo/quotaScoring.ts"; +import { registerQuotaFetcher, getQuotaFetcher } from "../../open-sse/services/quotaPreflight.ts"; +import { resolveProviderId } from "../../src/shared/constants/providers.ts"; +import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts"; + +function buildTarget(provider: string): ResolvedComboTarget { + return { + kind: "model", + stepId: "s1", + executionKey: "e1", + modelStr: `${provider}/some-model`, + provider, + providerId: provider, + connectionId: "conn-1", + weight: 1, + label: null, + } as ResolvedComboTarget; +} + +test("#10877: getResetAwareProvider() canonicalizes an alias-spelled provider so the fetcher registered under the canonical id is found", () => { + registerQuotaFetcher("ollama-cloud", async () => ({ ok: true }) as never); + + const target = buildTarget("ollamacloud"); + const lookedUpProvider = getResetAwareProvider(target); + + assert.equal( + lookedUpProvider, + resolveProviderId("ollamacloud"), + "getResetAwareProvider() should return the canonical provider id, not the raw alias" + ); + + const fetcher = getQuotaFetcher(lookedUpProvider!); + assert.notEqual( + fetcher, + undefined, + "a fetcher registered under the canonical provider id must be found for an alias-spelled combo target" + ); +}); + +test("#10877: getResetAwareProvider() is a no-op (same cache key) for already-canonical provider ids", () => { + registerQuotaFetcher("codex", async () => ({ ok: true }) as never); + + const target = buildTarget("codex"); + const lookedUpProvider = getResetAwareProvider(target); + + assert.equal(lookedUpProvider, "codex"); + assert.notEqual(getQuotaFetcher(lookedUpProvider!), undefined); +}); + +test("#10877: getResetAwareProvider() returns null when neither providerId nor provider is set", () => { + const target = { + kind: "model", + stepId: "s1", + executionKey: "e1", + modelStr: "unknown/model", + provider: "", + providerId: "", + connectionId: "conn-1", + weight: 1, + label: null, + } as ResolvedComboTarget; + + assert.equal(getResetAwareProvider(target), null); +}); From 19741775eeddca3728b1d0d1504555b86162bb73 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:42:02 -0300 Subject: [PATCH 127/135] fix(sse): strip commentary items from Responses response.completed snapshot (#10156) Live SSE frames for a phase:"commentary" message were already dropped per #6199, but the terminal response.completed.response.output array was forwarded verbatim whenever the upstream echoed the same item back non-empty, since backfillResponsesCompletedOutput only fills an empty array. Reuse the existing isResponsesCommentaryMessageItem predicate to filter the terminal snapshot's output array (and, defensively, the backfill buffer it can be seeded from) so both representations agree. Regression test added to tests/unit/responses-commentary-passthrough-6199.test.ts reproducing the exact upstream shape from the issue. --- ...responses-commentary-completed-snapshot.md | 1 + open-sse/utils/responsesStreamHelpers.ts | 23 +++++++ open-sse/utils/stream.ts | 32 +++++++++- ...ponses-commentary-passthrough-6199.test.ts | 60 +++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10156-responses-commentary-completed-snapshot.md diff --git a/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md new file mode 100644 index 0000000000..7a976ab85d --- /dev/null +++ b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md @@ -0,0 +1 @@ +- **fix(sse):** Responses-passthrough `response.completed` snapshots now drop `phase:"commentary"` items the same way live SSE frames already do, so the terminal `response.output` array no longer echoes internal commentary text that was already suppressed from the stream (#10156). diff --git a/open-sse/utils/responsesStreamHelpers.ts b/open-sse/utils/responsesStreamHelpers.ts index ce40999eb8..a2cba80fc1 100644 --- a/open-sse/utils/responsesStreamHelpers.ts +++ b/open-sse/utils/responsesStreamHelpers.ts @@ -121,6 +121,29 @@ export function pushUniqueResponsesOutputItems(target: unknown[], items: readonl } } +/** + * #10156 — strip items matched by `isCommentaryItem` (the same predicate used + * to drop live commentary-phase SSE frames, #6199) from a `response.completed` + * output array before it is forwarded or buffered for backfill. Upstreams may + * echo an already-dropped commentary item back inside a non-empty terminal + * `output` array; without this, the live stream and the terminal snapshot + * silently disagree about what the client actually saw. + */ +export function filterResponsesCommentaryFromItems( + items: readonly unknown[], + isCommentaryItem: (item: unknown) => boolean +): { items: unknown[]; changed: boolean } { + let changed = false; + const filtered = items.filter((item) => { + if (isCommentaryItem(item)) { + changed = true; + return false; + } + return true; + }); + return { items: filtered, changed }; +} + export function backfillResponsesCompletedOutput( parsed: unknown, collectedItems: readonly unknown[] diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 275669e983..4eab8ea7fc 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -36,6 +36,7 @@ import { import { STREAM_IDLE_TIMEOUT_MS, FETCH_BODY_TIMEOUT_MS, HTTP_STATUS } from "../config/constants.ts"; import { OMIT_STREAMING_CHUNK_MARKER, + isResponsesCommentaryMessageItem, sanitizeStreamingChunk, } from "../handlers/responseSanitizer.ts"; import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; @@ -59,6 +60,7 @@ import { } from "../services/sessionManager.ts"; import { backfillResponsesCompletedOutput, + filterResponsesCommentaryFromItems, normalizeResponsesCompletedUsage as normalizeUsage, normalizeResponsesSseIds, pushUniqueResponsesOutputItems, @@ -1565,11 +1567,26 @@ export function createSSEStream(options: StreamOptions = {}) { } } } + let responsesCommentaryStrippedFromCompleted = false; if ( parsed.type === "response.completed" && Array.isArray(parsed.response?.output) && parsed.response.output.length > 0 ) { + // #10156 — an upstream may echo a `phase:"commentary"` item back + // inside a non-empty terminal `output` array even though its live + // SSE frames were already dropped above. Keep both representations + // consistent by applying the same drop here. + if (shouldDropResponsesCommentary) { + const { items, changed } = filterResponsesCommentaryFromItems( + parsed.response.output, + isResponsesCommentaryMessageItem + ); + if (changed) { + parsed.response.output = items; + responsesCommentaryStrippedFromCompleted = true; + } + } pushUniqueResponsesOutputItems( passthroughResponsesOutputItems, parsed.response.output @@ -1613,9 +1630,19 @@ export function createSSEStream(options: StreamOptions = {}) { ]) as typeof parsed; } const stripped = stripResponsesLifecycleEcho(parsed); + // Belt-and-suspenders for #10156: filter the backfill buffer itself + // before it can seed an empty `response.completed.response.output`, + // in case a future code path pushes a commentary item into it + // without going through the response.completed branch above. + const backfillCandidates = shouldDropResponsesCommentary + ? filterResponsesCommentaryFromItems( + passthroughResponsesOutputItems, + isResponsesCommentaryMessageItem + ).items + : passthroughResponsesOutputItems; const backfilled = backfillResponsesCompletedOutput( parsed, - passthroughResponsesOutputItems + backfillCandidates ); const usageNormalized = normalizeUsage(parsed); if ( @@ -1623,7 +1650,8 @@ export function createSSEStream(options: StreamOptions = {}) { backfilled || textualToolCallBackfilled || responsesIdsNormalized || - usageNormalized + usageNormalized || + responsesCommentaryStrippedFromCompleted ) { output = `data: ${JSON.stringify(parsed)}\n\n`; injectedUsage = true; diff --git a/tests/unit/responses-commentary-passthrough-6199.test.ts b/tests/unit/responses-commentary-passthrough-6199.test.ts index 2b041c48d8..c1fce80786 100644 --- a/tests/unit/responses-commentary-passthrough-6199.test.ts +++ b/tests/unit/responses-commentary-passthrough-6199.test.ts @@ -364,3 +364,63 @@ test("Claude to Responses translation includes canonical Codex usage", async () assert.equal(completed.response.usage.output_tokens, 6); assert.equal(completed.response.usage.total_tokens, 94); }); + +// #10156 — the live-frame drop above works correctly, but real upstreams (as in +// the issue's repro) echo the ALREADY-DROPPED commentary item back inside the +// terminal `response.completed.response.output` array. Because that array is +// non-empty, `backfillResponsesCompletedOutput` never touches it, so the +// terminal snapshot silently disagreed with the events already delivered to +// the client. This must stay filtered too. +test("response.completed strips a commentary item the upstream echoes back non-empty (#10156)", async () => { + const output = await readTransformed( + [ + ...buildResponsesStream().slice(0, -1), + sse({ + type: "response.completed", + response: { + id: "resp_10156", + output: [ + { + id: "msg_commentary", + type: "message", + role: "assistant", + phase: "commentary", + content: [{ type: "output_text", text: COMMENTARY_TEXT }], + }, + { + id: "msg_final", + type: "message", + role: "assistant", + phase: "final", + content: [{ type: "output_text", text: FINAL_TEXT }], + }, + ], + }, + }), + ], + { ...PASSTHROUGH_RESPONSES_OPTIONS, dropResponsesCommentary: true } + ); + + assert.ok( + !output.includes(COMMENTARY_TEXT), + "commentary text must never reach the client, live or in the terminal snapshot" + ); + assert.ok( + !output.includes("msg_commentary"), + "the commentary item id must not appear anywhere in the forwarded stream" + ); + + const completedLine = output + .split(/\r?\n/) + .find((line) => line.startsWith("data:") && line.includes('"response.completed"')); + assert.ok(completedLine, "the terminal Responses event must be forwarded"); + const completed = JSON.parse(completedLine.slice(5).trim()); + assert.ok( + !completed.response.output.some((item: { phase?: string }) => item.phase === "commentary"), + "BUG #10156: response.completed.response.output must not retain the commentary item once its live SSE frames were suppressed — live stream and terminal snapshot must stay consistent" + ); + assert.ok( + completed.response.output.some((item: { id?: string }) => item.id === "msg_final"), + "the final answer item must still be present in the terminal snapshot" + ); +}); From 3fed9e837a10d81a0a4f0bdc86cb04f3481f976c Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 21 Aug 2026 08:37:54 +0800 Subject: [PATCH 128/135] fix(sse): add the missing minimax-music dispatch to music generation (#10650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obrigado — bug real: MUSIC_PROVIDERS.minimax declara format "minimax-music" e seus modelos são publicados pelo catálogo, mas handleMusicGeneration nunca teve um branch para esse format — todo request minimax/* caía no guard final com "Unsupported music format", modelos anunciados mas inalcançáveis. Handler completo cobrindo os dois output formats (url/hex), envelope base_resp, endpoint regional, e guarda local de credencial ausente. Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos): - typecheck:core limpo, complexity/cognitive-complexity dentro do baseline - tests/unit/minimax-music-generation.test.ts — 9/9 passando --- .../minimax-music-generation-dispatch.md | 1 + open-sse/config/musicRegistry.ts | 11 +- .../handlers/mediaGeneration/minimaxMusic.ts | 358 ++++++++++++++++++ open-sse/handlers/musicGeneration.ts | 12 + tests/unit/minimax-music-generation.test.ts | 267 +++++++++++++ 5 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/minimax-music-generation-dispatch.md create mode 100644 open-sse/handlers/mediaGeneration/minimaxMusic.ts create mode 100644 tests/unit/minimax-music-generation.test.ts diff --git a/changelog.d/fixes/minimax-music-generation-dispatch.md b/changelog.d/fixes/minimax-music-generation-dispatch.md new file mode 100644 index 0000000000..e0cf2114ca --- /dev/null +++ b/changelog.d/fixes/minimax-music-generation-dispatch.md @@ -0,0 +1 @@ +- **fix(sse):** MiniMax music models now generate audio instead of failing with `Unsupported music format: minimax-music` — the provider entry was registered in the music registry (and advertised by `/v1/models`), but `handleMusicGeneration` had no branch for its format, so every `minimax/*` music request fell through the dispatch chain to a 400. Adds the missing dispatch: a single synchronous POST with the `base_resp` envelope check (a non-zero `status_code` arrives on HTTP 200 too), `data.status` handling (an unfinished generation is reported instead of polled — the operation has no task id and no query endpoint), `url` and `hex` output formats (hex normalized to base64), `mp3`/`wav`/`pcm` containers via `audio_setting`, and the regional endpoint through the per-connection base-URL override, which is also the only host that accepts `aigc_watermark`. The registry entry gains the generation and cover model ids it was missing and drops a query URL that does not exist for this operation. Regression guard: `tests/unit/minimax-music-generation.test.ts` (9 tests). diff --git a/open-sse/config/musicRegistry.ts b/open-sse/config/musicRegistry.ts index 06aa8a159b..fd1f88fd88 100644 --- a/open-sse/config/musicRegistry.ts +++ b/open-sse/config/musicRegistry.ts @@ -17,6 +17,8 @@ interface MusicProvider { id: string; baseUrl: string; statusUrl?: string; + /** Regional deployment of the same contract, reachable via a base-URL override. */ + regionalBaseUrl?: string; authType: string; authHeader: string; format: string; @@ -79,14 +81,21 @@ export const MUSIC_PROVIDERS: Record = { minimax: { id: "minimax", baseUrl: "https://api.minimax.io/v1/music_generation", - statusUrl: "https://api.minimax.io/v1/query/music_generation", + // The music operation answers with the finished audio in the POST response — + // there is no task id and no query endpoint, hence no statusUrl. The regional + // deployment serves the same contract and is the only host that accepts the + // `aigc_watermark` request field. + regionalBaseUrl: "https://api.minimaxi.com/v1/music_generation", authType: "apikey", authHeader: "bearer", format: "minimax-music", models: [ + { id: "music-3.0", name: "Music 3.0" }, { id: "music-2.6", name: "Music 2.6" }, + { id: "music-3.0-free", name: "Music 3.0 Free" }, { id: "music-2.6-free", name: "Music 2.6 Free" }, { id: "music-cover", name: "Music Cover" }, + { id: "music-cover-free", name: "Music Cover Free" }, ], }, comfyui: { diff --git a/open-sse/handlers/mediaGeneration/minimaxMusic.ts b/open-sse/handlers/mediaGeneration/minimaxMusic.ts new file mode 100644 index 0000000000..3486676058 --- /dev/null +++ b/open-sse/handlers/mediaGeneration/minimaxMusic.ts @@ -0,0 +1,358 @@ +/** + * MiniMax music generation handler (format: "minimax-music"). + * + * The provider entry has been in musicRegistry since the media registries were + * introduced, but handleMusicGeneration never grew a branch for its format — so + * every registered `minimax/*` music model fell through the dispatch chain to + * `Unsupported music format: minimax-music` (400) and the models were + * advertised by /v1/models while being impossible to call. + * + * The upstream contract is a single synchronous POST — unlike the vendor's + * task-based media endpoints there is no task id and no query endpoint, so a + * request is either finished (`data.status` 2, audio in `data.audio`) or still + * generating (`data.status` 1), which can only be reported back, never awaited. + * Failures are carried in the `base_resp` envelope (`status_code` 0 = success) + * even on HTTP 200. + * + * `output_format` selects how the audio comes back: `url` (a short-lived link, + * valid for 24h — callers must download it before it expires) or `hex` (the raw + * container inline, normalized here to base64 so the response matches the + * OpenAI-shaped payload the other music branches return). + */ + +import { saveCallLog } from "@/lib/usageDb"; +import { sanitizeErrorMessage } from "../../utils/error.ts"; + +type MinimaxMusicBody = Record; + +interface MinimaxMusicProviderConfig { + baseUrl: string; + /** Regional deployment of the same contract — see resolveEndpoint below. */ + regionalBaseUrl?: string; +} + +interface MinimaxMusicCredentials { + apiKey?: unknown; + accessToken?: unknown; + providerSpecificData?: { baseUrl?: unknown } | null; +} + +interface MinimaxMusicLog { + info?: (scope: string, message: string) => void; + error?: (scope: string, message: string) => void; +} + +interface MinimaxMusicArgs { + model: string; + provider: string; + providerConfig: MinimaxMusicProviderConfig; + body: MinimaxMusicBody; + credentials?: MinimaxMusicCredentials | null; + log?: MinimaxMusicLog | null; +} + +/** Containers accepted by `audio_setting.format`. */ +const AUDIO_FORMATS = new Set(["mp3", "wav", "pcm"]); +/** Accepted `output_format` values. */ +const OUTPUT_FORMATS = new Set(["url", "hex"]); +/** Container assumed when the request does not pin `audio_setting.format`. */ +const DEFAULT_AUDIO_FORMAT = "mp3"; +/** `data.status`: 1 = still generating, 2 = finished. */ +const STATUS_IN_PROGRESS = 1; +/** String request fields forwarded verbatim when the caller provides them. */ +const STRING_REQUEST_FIELDS = [ + "prompt", + "lyrics", + "audio_url", + "audio_base64", + "cover_feature_id", +] as const; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function numberValue(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function booleanValue(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined; +} + +/** Fire-and-forget usage log for a MiniMax music-generation call. */ +function logMinimaxMusicCall(params: { + status: number; + model: string; + provider: string; + duration: number; + error?: string; + requestBody?: unknown; + responseBody?: unknown; +}): void { + saveCallLog({ + method: "POST", + path: "/v1/music/generations", + ...params, + }).catch(() => {}); +} + +/** + * Endpoint for this call: the per-connection `providerSpecificData.baseUrl` + * override (the same storage every configurable-base-URL provider uses) wins + * over the registry default. That override is how a connection targets the + * regional deployment declared as `regionalBaseUrl`. + */ +function resolveEndpoint( + providerConfig: MinimaxMusicProviderConfig, + credentials?: MinimaxMusicCredentials | null +): string { + const psd = credentials?.providerSpecificData; + const override = isRecord(psd) ? stringValue(psd.baseUrl) : undefined; + return override || providerConfig.baseUrl; +} + +/** True when `endpoint` is the regional deployment declared by the registry. */ +function isRegionalEndpoint(endpoint: string, regionalBaseUrl?: string): boolean { + if (!regionalBaseUrl) return false; + try { + return new URL(endpoint).host === new URL(regionalBaseUrl).host; + } catch { + return false; + } +} + +/** Forwards only the recognized `audio_setting` members, dropping unknown containers. */ +function buildAudioSetting(body: MinimaxMusicBody): Record | undefined { + const provided: Record = isRecord(body.audio_setting) ? body.audio_setting : {}; + const setting: Record = {}; + + const sampleRate = numberValue(provided.sample_rate); + if (sampleRate !== undefined) setting.sample_rate = sampleRate; + + const bitrate = numberValue(provided.bitrate); + if (bitrate !== undefined) setting.bitrate = bitrate; + + const format = stringValue(provided.format)?.toLowerCase(); + if (format && AUDIO_FORMATS.has(format)) setting.format = format; + + return Object.keys(setting).length > 0 ? setting : undefined; +} + +/** Container reported back to the caller — mirrors what was asked upstream. */ +function resolveAudioFormat(body: MinimaxMusicBody): string { + const provided: Record = isRecord(body.audio_setting) ? body.audio_setting : {}; + const format = stringValue(provided.format)?.toLowerCase(); + return format && AUDIO_FORMATS.has(format) ? format : DEFAULT_AUDIO_FORMAT; +} + +function resolveOutputFormat(body: MinimaxMusicBody): string { + const requested = stringValue(body.output_format)?.toLowerCase(); + return requested && OUTPUT_FORMATS.has(requested) ? requested : "url"; +} + +/** + * Upstream request body. `stream` is pinned false: this route answers with a + * single JSON payload, and streaming responses would also be restricted to the + * hex output format. + */ +function buildUpstreamBody( + model: string, + body: MinimaxMusicBody, + regional: boolean +): Record { + const request: Record = { + model, + stream: false, + output_format: resolveOutputFormat(body), + }; + + for (const field of STRING_REQUEST_FIELDS) { + const value = stringValue(body[field]); + if (value !== undefined) request[field] = value; + } + + const audioSetting = buildAudioSetting(body); + if (audioSetting) request.audio_setting = audioSetting; + + const lyricsOptimizer = booleanValue(body.lyrics_optimizer); + if (lyricsOptimizer !== undefined) request.lyrics_optimizer = lyricsOptimizer; + + // `instrumental` is the spelling the other music branches already accept. + const isInstrumental = booleanValue(body.is_instrumental) ?? booleanValue(body.instrumental); + if (isInstrumental !== undefined) request.is_instrumental = isInstrumental; + + // Only the regional endpoint accepts a watermark flag. + if (regional) { + const watermark = booleanValue(body.aigc_watermark); + if (watermark !== undefined) request.aigc_watermark = watermark; + } + + return request; +} + +async function readPayload(response: Response): Promise> { + const rawText = await response.text(); + if (!rawText) return {}; + try { + const parsed: unknown = JSON.parse(rawText); + return isRecord(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +/** Hex payloads are normalized to base64; Buffer would silently drop bad nibbles. */ +function hexAudioToBase64(audioHex: string): string { + if (audioHex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(audioHex)) { + throw new Error("MiniMax music generation returned invalid hex audio"); + } + return Buffer.from(audioHex, "hex").toString("base64"); +} + +/** `base_resp.status_code` is non-zero on failures that still answer HTTP 200. */ +function readEnvelopeError(payload: Record): string | undefined { + const baseResp: Record = isRecord(payload.base_resp) ? payload.base_resp : {}; + const statusCode = numberValue(baseResp.status_code); + if (statusCode === undefined || statusCode === 0) return undefined; + return stringValue(baseResp.status_msg) || `upstream status code ${statusCode}`; +} + +export async function handleMinimaxMusicGeneration({ + model, + provider, + providerConfig, + body, + credentials, + log, +}: MinimaxMusicArgs) { + const startTime = Date.now(); + const token = stringValue(credentials?.apiKey) || stringValue(credentials?.accessToken); + if (!token) { + return { success: false as const, status: 401, error: "MiniMax API key is required" }; + } + + const modelId = stringValue(model); + if (!modelId) { + return { success: false as const, status: 400, error: "MiniMax music model is required" }; + } + + const endpoint = resolveEndpoint(providerConfig, credentials); + const upstreamBody = buildUpstreamBody( + modelId, + body, + isRegionalEndpoint(endpoint, providerConfig.regionalBaseUrl) + ); + const audioFormat = resolveAudioFormat(body); + const modelLabel = `${provider}/${modelId}`; + + log?.info?.( + "MUSIC", + `${modelLabel} (minimax-music) | prompt: "${String(body.prompt ?? "").slice(0, 60)}..." | ` + + `output_format: ${upstreamBody.output_format} | audio_format: ${audioFormat}` + ); + + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(upstreamBody), + }); + + const payload = await readPayload(response); + + if (!response.ok) { + const errorMessage = + readEnvelopeError(payload) || `MiniMax music generation failed (${response.status})`; + log?.error?.("MUSIC", `${provider} minimax-music error ${response.status}: ${errorMessage}`); + logMinimaxMusicCall({ + status: response.status, + model: modelLabel, + provider, + duration: Date.now() - startTime, + error: errorMessage, + requestBody: upstreamBody, + }); + return { success: false as const, status: response.status, error: errorMessage }; + } + + const envelopeError = readEnvelopeError(payload); + if (envelopeError) { + log?.error?.("MUSIC", `${provider} minimax-music rejected the request: ${envelopeError}`); + logMinimaxMusicCall({ + status: 502, + model: modelLabel, + provider, + duration: Date.now() - startTime, + error: envelopeError, + requestBody: upstreamBody, + }); + return { success: false as const, status: 502, error: envelopeError }; + } + + const data: Record = isRecord(payload.data) ? payload.data : {}; + + // No task id and no query endpoint exist for this operation, so an + // unfinished generation cannot be polled — surface it instead of hanging. + if (numberValue(data.status) === STATUS_IN_PROGRESS) { + const pending = "MiniMax music generation is still in progress; retry the request"; + logMinimaxMusicCall({ + status: 502, + model: modelLabel, + provider, + duration: Date.now() - startTime, + error: pending, + }); + return { success: false as const, status: 502, error: pending }; + } + + const audio = stringValue(data.audio); + if (!audio) { + const errorMessage = "MiniMax music generation returned no audio"; + logMinimaxMusicCall({ + status: 502, + model: modelLabel, + provider, + duration: Date.now() - startTime, + error: errorMessage, + }); + return { success: false as const, status: 502, error: errorMessage }; + } + + const track = + upstreamBody.output_format === "hex" + ? { b64_json: hexAudioToBase64(audio), format: audioFormat } + : { url: audio, format: audioFormat }; + + logMinimaxMusicCall({ + status: 200, + model: modelLabel, + provider, + duration: Date.now() - startTime, + responseBody: { audio_count: 1 }, + }); + + return { + success: true as const, + data: { created: Math.floor(Date.now() / 1000), data: [track] }, + }; + } catch (err: unknown) { + const errorMessage = sanitizeErrorMessage(err) || "Music provider error"; + log?.error?.("MUSIC", `${provider} minimax-music error: ${errorMessage}`); + logMinimaxMusicCall({ + status: 502, + model: modelLabel, + provider, + duration: Date.now() - startTime, + error: errorMessage, + }); + return { success: false as const, status: 502, error: errorMessage }; + } +} diff --git a/open-sse/handlers/musicGeneration.ts b/open-sse/handlers/musicGeneration.ts index 92ee333c2e..89052b3765 100644 --- a/open-sse/handlers/musicGeneration.ts +++ b/open-sse/handlers/musicGeneration.ts @@ -33,6 +33,7 @@ import { } from "../utils/kieTask.ts"; import { sanitizeErrorMessage } from "../utils/error.ts"; import { handleFalMusicGeneration } from "./mediaGeneration/fal.ts"; +import { handleMinimaxMusicGeneration } from "./mediaGeneration/minimaxMusic.ts"; function normalizeKieSunoModel(model: string): string { const map: Record = { @@ -153,6 +154,17 @@ export async function handleMusicGeneration({ body, credentials, log }) { return handleUdioMusicGeneration({ model, provider, providerConfig, body, credentials, log }); } + if (providerConfig.format === "minimax-music") { + return handleMinimaxMusicGeneration({ + model, + provider, + providerConfig, + body, + credentials, + log, + }); + } + return { success: false, status: 400, diff --git a/tests/unit/minimax-music-generation.test.ts b/tests/unit/minimax-music-generation.test.ts new file mode 100644 index 0000000000..14ef8ef975 --- /dev/null +++ b/tests/unit/minimax-music-generation.test.ts @@ -0,0 +1,267 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-minimax-music-")); + +const { handleMusicGeneration } = await import("../../open-sse/handlers/musicGeneration.ts"); +const { MUSIC_PROVIDERS } = await import("../../open-sse/config/musicRegistry.ts"); + +const GLOBAL_ENDPOINT = MUSIC_PROVIDERS.minimax.baseUrl; +const REGIONAL_ENDPOINT = MUSIC_PROVIDERS.minimax.regionalBaseUrl as string; + +interface Captured { + url: string; + authorization: string; + contentType: string; + body: Record; +} + +/** Installs a fetch stub answering every call with `payload`, capturing the request. */ +function stubFetch(payload: unknown, status = 200) { + const captured: Captured[] = []; + const originalFetch = globalThis.fetch; + + globalThis.fetch = (async (url: string | URL | Request, options: RequestInit = {}) => { + const headers = new Headers(options.headers ?? {}); + captured.push({ + url: String(url), + authorization: headers.get("authorization") ?? "", + contentType: headers.get("content-type") ?? "", + body: JSON.parse(String(options.body ?? "{}")) as Record, + }); + return new Response(JSON.stringify(payload), { + status, + headers: { "content-type": "application/json" }, + }); + }) as typeof globalThis.fetch; + + return { + captured, + restore() { + globalThis.fetch = originalFetch; + }, + }; +} + +test("minimax is registered with the music models it serves and no query endpoint", () => { + const provider = MUSIC_PROVIDERS.minimax; + const modelIds = provider.models.map((model) => model.id); + + assert.equal(provider.format, "minimax-music"); + assert.equal(provider.authHeader, "bearer"); + assert.equal(provider.statusUrl, undefined); + assert.ok(REGIONAL_ENDPOINT, "a regional endpoint must be declared"); + assert.notEqual(new URL(REGIONAL_ENDPOINT).host, new URL(GLOBAL_ENDPOINT).host); + assert.deepEqual(modelIds, [ + "music-3.0", + "music-2.6", + "music-3.0-free", + "music-2.6-free", + "music-cover", + "music-cover-free", + ]); +}); + +test("handleMusicGeneration dispatches minimax-music and normalizes the audio URL", async () => { + const stub = stubFetch({ + data: { status: 2, audio: "https://example.com/minimax-music.mp3" }, + base_resp: { status_code: 0, status_msg: "success" }, + }); + + try { + const result = await handleMusicGeneration({ + body: { + model: "minimax/music-3.0", + prompt: "warm lo-fi guitar loop", + lyrics: "##first line\nsecond line##", + is_instrumental: false, + lyrics_optimizer: true, + audio_setting: { format: "wav", sample_rate: 44100, bitrate: 256000, bogus: "drop-me" }, + aigc_watermark: true, + }, + credentials: { apiKey: "minimax-key" }, + log: null, + }); + + assert.equal(stub.captured.length, 1); + const request = stub.captured[0]; + assert.equal(request.url, GLOBAL_ENDPOINT); + assert.equal(request.authorization, "Bearer minimax-key"); + assert.equal(request.contentType, "application/json"); + assert.equal(request.body.model, "music-3.0"); + assert.equal(request.body.prompt, "warm lo-fi guitar loop"); + assert.equal(request.body.lyrics, "##first line\nsecond line##"); + assert.equal(request.body.stream, false); + assert.equal(request.body.output_format, "url"); + assert.equal(request.body.is_instrumental, false); + assert.equal(request.body.lyrics_optimizer, true); + assert.deepEqual(request.body.audio_setting, { + sample_rate: 44100, + bitrate: 256000, + format: "wav", + }); + // The watermark field only exists on the regional endpoint. + assert.ok(!("aigc_watermark" in request.body)); + + assert.equal(result.success, true); + assert.deepEqual(result.data.data, [ + { url: "https://example.com/minimax-music.mp3", format: "wav" }, + ]); + } finally { + stub.restore(); + } +}); + +test("minimax-music forwards cover inputs and honors the hex output format", async () => { + const stub = stubFetch({ + data: { status: 2, audio: "48656c6c6f" }, + base_resp: { status_code: 0 }, + }); + + try { + const result = await handleMusicGeneration({ + body: { + model: "minimax/music-cover", + prompt: "cover this take", + output_format: "HEX", + audio_url: "https://example.com/reference.mp3", + cover_feature_id: "feature-1", + }, + credentials: { accessToken: "minimax-token" }, + log: null, + }); + + const request = stub.captured[0]; + assert.equal(request.body.model, "music-cover"); + assert.equal(request.body.output_format, "hex"); + assert.equal(request.body.audio_url, "https://example.com/reference.mp3"); + assert.equal(request.body.cover_feature_id, "feature-1"); + + assert.equal(result.success, true); + assert.deepEqual(result.data.data, [ + { b64_json: Buffer.from("Hello").toString("base64"), format: "mp3" }, + ]); + } finally { + stub.restore(); + } +}); + +test("minimax-music targets the regional endpoint via the connection base URL", async () => { + const stub = stubFetch({ + data: { status: 2, audio: "https://example.com/regional.mp3" }, + base_resp: { status_code: 0 }, + }); + + try { + const result = await handleMusicGeneration({ + body: { model: "minimax/music-2.6", prompt: "guzheng ballad", aigc_watermark: true }, + credentials: { + apiKey: "minimax-key", + providerSpecificData: { baseUrl: REGIONAL_ENDPOINT }, + }, + log: null, + }); + + const request = stub.captured[0]; + assert.equal(request.url, REGIONAL_ENDPOINT); + assert.equal(request.body.aigc_watermark, true); + assert.equal(result.success, true); + } finally { + stub.restore(); + } +}); + +test("minimax-music surfaces base_resp failures returned with HTTP 200", async () => { + const stub = stubFetch({ base_resp: { status_code: 1004, status_msg: "invalid api key" } }); + + try { + const logged: string[] = []; + const result = await handleMusicGeneration({ + body: { model: "minimax/music-3.0", prompt: "x" }, + credentials: { apiKey: "minimax-key" }, + log: { info: () => {}, error: (_scope: string, message: string) => logged.push(message) }, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 502); + assert.equal(result.error, "invalid api key"); + assert.equal(logged.length, 1); + } finally { + stub.restore(); + } +}); + +test("minimax-music reports an unfinished generation instead of polling", async () => { + const stub = stubFetch({ data: { status: 1 }, base_resp: { status_code: 0 } }); + + try { + const result = await handleMusicGeneration({ + body: { model: "minimax/music-3.0-free", prompt: "x" }, + credentials: { apiKey: "minimax-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 502); + assert.match(result.error, /still in progress/); + } finally { + stub.restore(); + } +}); + +test("minimax-music rejects a completed response that carries no audio", async () => { + const stub = stubFetch({ data: { status: 2 }, base_resp: { status_code: 0 } }); + + try { + const result = await handleMusicGeneration({ + body: { model: "minimax/music-3.0", prompt: "x" }, + credentials: { apiKey: "minimax-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 502); + assert.match(result.error, /returned no audio/); + } finally { + stub.restore(); + } +}); + +test("minimax-music propagates upstream HTTP failures", async () => { + const stub = stubFetch({ base_resp: { status_code: 2013, status_msg: "invalid params" } }, 400); + + try { + const result = await handleMusicGeneration({ + body: { model: "minimax/music-3.0", prompt: "x" }, + credentials: { apiKey: "minimax-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 400); + assert.equal(result.error, "invalid params"); + } finally { + stub.restore(); + } +}); + +test("minimax-music refuses to call upstream without a credential", async () => { + const stub = stubFetch({}); + + try { + const result = await handleMusicGeneration({ + body: { model: "minimax/music-3.0", prompt: "x" }, + credentials: null, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 401); + assert.equal(stub.captured.length, 0); + } finally { + stub.restore(); + } +}); From 118840131d66223c37156ae773660cd2cc767006 Mon Sep 17 00:00:00 2001 From: Webman Date: Thu, 20 Aug 2026 20:02:14 -0500 Subject: [PATCH 129/135] fix(deps): upgrade @atjsh/llmlingua-2 to 2.0.5 and drop @tensorflow/tfjs (#10610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #10536: upgrade @atjsh/llmlingua-2 2.0.3 → 2.0.5 and drop @tensorflow/tfjs from the LLMLingua SLM optional stack. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 20 files): - 48/48 focused llmlingua/colocate/docker unit tests pass (author-reported, reproduced). - check-file-size, check-changelog-integrity: OK. - grep confirms no remaining source imports of @tensorflow/tfjs. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: jonlwheat2-gif --- .github/dependabot.yml | 14 +- Dockerfile | 2 +- .../10536-llmlingua-2-2.0.5-drop-tfjs.md | 1 + config/quality/dependency-allowlist.json | 1 - docs/compression/COMPRESSION_ENGINES.md | 43 +- .../docs/compression/COMPRESSION_ENGINES.md | 31 +- docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md | 8 +- docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md | 10 +- docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md | 10 +- docs/ops/RELEASE_CHECKLIST.md | 10 +- .../compression/engines/llmlingua/worker.ts | 6 +- package-lock.json | 367 +----------------- package.json | 3 +- scripts/build/colocate-standalone.mjs | 5 +- scripts/build/colocateOptionals.mjs | 17 +- scripts/build/prepublish.ts | 2 +- scripts/packs/optionalPackManifest.mjs | 3 +- tests/unit/colocate-optionals.test.ts | 47 +-- .../unit/compression/llmlingua-worker.test.ts | 7 +- .../docker-llmlingua-optionals-9166.test.ts | 27 +- 20 files changed, 107 insertions(+), 507 deletions(-) create mode 100644 changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8db8504007..3dfad903a7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -50,13 +50,13 @@ updates: # bumps; majors here need their own PR and a deliberate migration review. - dependency-name: "ioredis" update-types: ["version-update:semver-major"] - # @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN. - # It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/ - # compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2) - # and for local memory embeddings (src/lib/memory/embedding/transformersLocal.ts), - # and was VPS-validated at 3.5.2 (#4014). 4.x breaks both, and even 3.x minors must - # be re-validated on the VPS — so freeze ALL auto-bumps (no update-types = ignore - # every version). Migrate it intentionally, not via dependabot (#4050). + # @huggingface/transformers is VPS-validated at ^4.2.0 (migrated intentionally in + # #9962). It is load-bearing for the LLMLingua ONNX compression engine (open-sse/ + # services/compression/engines/llmlingua/ — @atjsh/llmlingua-2@2.0.5 peers on + # "@huggingface/transformers": "^3.5.2 || ^4.0.0") and for local memory embeddings + # (src/lib/memory/embedding/transformersLocal.ts). Further majors must be re-validated + # on the VPS — so keep auto-bumps frozen (no update-types = ignore every version). + # Migrate it intentionally, not via dependabot (#4050). - dependency-name: "@huggingface/transformers" - package-ecosystem: "github-actions" diff --git a/Dockerfile b/Dockerfile index de9b5a1499..8eca2c3bd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -173,7 +173,7 @@ COPY . ./ RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-next-cache,target=/app/.build/next/cache \ mkdir -p /app/data \ && npm run build \ - && node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);" + && node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);" # ── Runner base ──────────────────────────────────────────────────────────── FROM base AS runner-base diff --git a/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md b/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md new file mode 100644 index 0000000000..11a1845715 --- /dev/null +++ b/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md @@ -0,0 +1 @@ +- **fix(deps):** upgrade `@atjsh/llmlingua-2` from 2.0.3 to 2.0.5 and remove `@tensorflow/tfjs` from the LLMLingua SLM stack — 2.0.5 adds official Transformers.js v4 support (peers `@huggingface/transformers` at `^3.5.2 || ^4.0.0`) and 2.0.4+ no longer requires TensorFlow.js, restoring compatibility with OmniRoute's Transformers.js v4 while dropping the largest single contributor to the optional runtime footprint ([#10536](https://github.com/diegosouzapw/OmniRoute/issues/10536)) diff --git a/config/quality/dependency-allowlist.json b/config/quality/dependency-allowlist.json index f276fc45d2..c4476f95d1 100644 --- a/config/quality/dependency-allowlist.json +++ b/config/quality/dependency-allowlist.json @@ -20,7 +20,6 @@ "@stryker-mutator/tap-runner", "@swc/helpers", "@tailwindcss/postcss", - "@tensorflow/tfjs", "@testing-library/jest-dom", "@testing-library/react", "@toon-format/toon", diff --git a/docs/compression/COMPRESSION_ENGINES.md b/docs/compression/COMPRESSION_ENGINES.md index 22cffe7c42..4a232a0e5d 100644 --- a/docs/compression/COMPRESSION_ENGINES.md +++ b/docs/compression/COMPRESSION_ENGINES.md @@ -28,12 +28,12 @@ The `omniglyph` engine (package `omniglyph`, 1.4.0+) accepts a named semantic pr globally through `omniglyph.profile` in the compression settings or per step through the stacked pipeline's step config: -| Profile | Boundary | -| -------------- | --------------------------------------------------------------------------- | -| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history | -| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history | -| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns | -| `passthrough` | Routes without transforming; the engine is skipped | +| Profile | Boundary | +| ------------- | ------------------------------------------------------------------------------------------------ | +| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history | +| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history | +| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns | +| `passthrough` | Routes without transforming; the engine is skipped | The profile is a **ceiling, not a floor**: `mergeCompressionProfileOptions` in the package refuses to let a caller override reopen a lossy lane the profile closed, so a per-step @@ -170,22 +170,22 @@ override points it at a local copy instead (offline / air-gapped installs). ### Optional dependencies & on-demand install -The prunable LLMLingua runtime peer stack is **optional**. Three packages are declared as +The prunable LLMLingua runtime peer stack is **optional**. Two packages are declared as `optionalDependencies` in `package.json` and kept **external** by the production build (`scripts/build/prepublish.ts` does not bundle them): -| Package | Version (pin) | Notes | -| -------------------- | ------------- | ---------------------------------------------- | -| `@atjsh/llmlingua-2` | `2.0.3` | Entry package; declares the others as peers | -| `@tensorflow/tfjs` | `4.22.0` | Heaviest dep — dominates the ~800 MB footprint | -| `js-tiktoken` | `^1.0.20` | Tokenizer | +| Package | Version (pin) | Notes | +| -------------------- | ------------- | ------------------------------------------- | +| `@atjsh/llmlingua-2` | `2.0.5` | Entry package; declares the others as peers | +| `js-tiktoken` | `^1.0.20` | Tokenizer | -`@huggingface/transformers` is pinned at `3.5.2` as an **optional** dependency (shared with -the local embeddings path and also traced into the standalone bundle). Keeping it optional prevents -`onnxruntime-node` CUDA provider postinstall failures on CUDA 11 hosts from aborting the whole -OmniRoute install; when the optional stack is absent, LLMLingua still fail-opens. Only the three -packages above are prunable SLM peers. A standard `npm install` (dev) installs the optional stack -automatically unless optional dependencies are omitted. +`@huggingface/transformers` is pinned at `^4.2.0` (shared with the local embeddings path and +also traced into the standalone bundle); `@atjsh/llmlingua-2@2.0.5` peers on it with +`"^3.5.2 || ^4.0.0"`, so both Transformers.js v3 and v4 are supported. Since 2.0.4, +`@atjsh/llmlingua-2` no longer requires `@tensorflow/tfjs`, which removed the largest single +contributor (TensorFlow.js) from the SLM stack. Only the two packages above are prunable SLM +peers. A standard `npm install` (dev) installs the optional stack automatically unless optional +dependencies are omitted. **Why on-demand:** the npm-published package, the standalone bundle, and the Docker image ship **without** these deps to stay slim. When they are absent, the worker's dependency @@ -195,11 +195,12 @@ error logged). To activate it in a pruned environment, install the optional stac ```bash # pin to the versions declared in package.json optionalDependencies -npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken +npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken ``` -Roughly **~800 MB** total: the TensorFlow.js + transformers runtimes dominate; the -TinyBERT model adds ~57 MB downloaded at first use (not via npm). +The `@tensorflow/tfjs` removal (2.0.4+) eliminates the previously dominant ~800 MB +contributor — the remaining footprint is the transformers.js + onnxruntime-node runtimes, +plus the TinyBERT model (~57 MB) downloaded at first use (not via npm). Per environment: diff --git a/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md b/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md index b8488d6872..ea266af05b 100644 --- a/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md +++ b/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md @@ -142,22 +142,22 @@ wskazuje zamiast tego lokalną kopię (instalacje offline / air-gapped). ### Opcjonalne zależności i instalacja on-demand -Przycinany stos peerów runtime LLMLingua jest **opcjonalny**. Trzy pakiety są zadeklarowane jako +Przycinany stos peerów runtime LLMLingua jest **opcjonalny**. Dwa pakiety są zadeklarowane jako `optionalDependencies` w `package.json` i utrzymywane jako **external** przez build produkcyjny (`scripts/build/prepublish.ts` ich nie bundluje): -| Package | Version (pin) | Notes | -| -------------------- | ------------- | ------------------------------------------------- | -| `@atjsh/llmlingua-2` | `2.0.3` | Pakiet wejściowy; deklaruje pozostałe jako peery | -| `@tensorflow/tfjs` | `4.22.0` | Najcięższa zależność — dominuje footprint ~800 MB | -| `js-tiktoken` | `^1.0.20` | Tokenizer | +| Package | Version (pin) | Notes | +| -------------------- | ------------- | ------------------------------------------- | +| `@atjsh/llmlingua-2` | `2.0.5` | Pakiet wejściowy; deklaruje pozostałe jako peery | +| `js-tiktoken` | `^1.0.20` | Tokenizer | -`@huggingface/transformers` jest pinowany na `3.5.2` jako **opcjonalna** zależność (współdzielona ze -ścieżką lokalnych embeddings i również śledzona do standalone bundle). Utrzymanie jej jako optional -zapobiega awariom postinstall providera CUDA `onnxruntime-node` na hostach CUDA 11, które przerywałyby -całą instalację OmniRoute; gdy opcjonalny stos jest nieobecny, LLMLingua nadal fail-openuje. Tylko trzy -powyższe pakiety to przycinane peery SLM. Standardowe `npm install` (dev) instaluje opcjonalny stos -automatycznie, o ile opcjonalne zależności nie zostaną pominięte. +`@huggingface/transformers` jest pinowany na `^4.2.0` (współdzielony ze ścieżką lokalnych embeddings +i również śledzony do standalone bundle); `@atjsh/llmlingua-2@2.0.5` peeruje na nim przez +`"^3.5.2 || ^4.0.0"`, więc obsługiwane są zarówno Transformers.js v3, jak i v4. Od 2.0.4 +`@atjsh/llmlingua-2` nie wymaga już `@tensorflow/tfjs`, co usunęło największy pojedynczy wkład +(TensorFlow.js) ze stosu SLM. Tylko dwa powyższe pakiety to przycinane peery SLM. Standardowe +`npm install` (dev) instaluje opcjonalny stos automatycznie, o ile opcjonalne zależności nie zostaną +pominięte. **Dlaczego on-demand:** pakiet publikowany w npm, standalone bundle i obraz Docker dostarczane są **bez** tych zależności, aby pozostać lekkie. Gdy ich brakuje, bramka zależności @@ -167,11 +167,12 @@ logowanego błędu). Aby aktywować go w przyciętym środowisku, zainstaluj opc ```bash # pin to the versions declared in package.json optionalDependencies -npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken +npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken ``` -Łącznie mniej więcej **~800 MB**: dominują runtime’y TensorFlow.js + transformers; model -TinyBERT dodaje ~57 MB pobierane przy pierwszym użyciu (nie przez npm). +Usunięcie `@tensorflow/tfjs` (2.0.4+) eliminuje wcześniej dominujący wkład ~800 MB — pozostały +footprint to runtime’y transformers.js + onnxruntime-node oraz model TinyBERT (~57 MB) pobierany +przy pierwszym użyciu (nie przez npm). Per środowisko: diff --git a/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md index 1d9cdb29f6..535dadd53c 100644 --- a/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/i18n/pl/docs/ops/RELEASE_CHECKLIST.md @@ -326,13 +326,11 @@ Przed wypuszczeniem dowolnego wydania v3.8.x zweryfikuj te dodatkowe pozycje: - [ ] `npm install -g omniroute@` uruchamia postinstall bez fatalnego wyjścia - [ ] Ścieżka update zachowuje optional deps: `omniroute update --apply` i auto-updater uruchamiają `npm install -g … --include=optional`, żeby `optionalDependencies` (better-sqlite3, - keytar, tls-client oraz stack SLM llmlingua: `@atjsh/llmlingua-2`, - `@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) przeżyły update. - `@huggingface/transformers` zostaje optional, żeby jego postinstall providera CUDA `onnxruntime-node` - nie mógł przerwać instalacji na hostach CUDA 11. Tier ultra `modelPath` SLM potrzebuje też + keytar, tls-client oraz stack SLM llmlingua: `@atjsh/llmlingua-2@2.0.5`, + `js-tiktoken`) przeżyły update. Tier ultra `modelPath` SLM potrzebuje też modelu tinybert, auto-pobieranego do `${DATA_DIR}/models/llmlingua` przy pierwszym użyciu. Postinstall (`scripts/build/colocateOptionals.mjs`) następnie ko-lokuje opcjonalne zamknięcie SLM do - `dist/node_modules`, żeby worker rozwiązywał JEDNĄ opcjonalną instancję `@huggingface/transformers` 3.5.2 + `dist/node_modules`, żeby worker rozwiązywał JEDNĄ instancję `@huggingface/transformers` ^4.2.0 — standalone trace bundluje tylko transformers, nie dynamicznie importowane optionals, więc bez tego worker załadowałby llmlingua-2 przeciw transformers z roota i tier SLM cicho fail-openowałby. diff --git a/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md index d8cefd6497..afdbf398af 100644 --- a/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md @@ -275,14 +275,12 @@ npm run build:release - [ ] `npm install -g omniroute@` 运行 postinstall 无致命退出 - [ ] 更新路径保留可选依赖:`omniroute update --apply` 以及自动更新器 运行 `npm install -g … --include=optional` 以确保 `optionalDependencies`(better-sqlite3、 - keytar、tls-client 以及 llmlingua SLM 栈:`@atjsh/llmlingua-2`、 - `@huggingface/transformers@3.5.2`、`@tensorflow/tfjs`、`js-tiktoken`)在更新后仍然存在。 - `@huggingface/transformers` 保持为可选依赖,这样其 `onnxruntime-node` CUDA provider postinstall - 不会在 CUDA 11 主机上中断安装。Ultra 模式的 `modelPath` SLM 层还需要 + keytar、tls-client 以及 llmlingua SLM 栈:`@atjsh/llmlingua-2@2.0.5`、 + `js-tiktoken`)在更新后仍然存在。Ultra 模式的 `modelPath` SLM 层还需要 tinybert 模型,首次使用时自动下载到 `${DATA_DIR}/models/llmlingua`。postinstall (`scripts/build/colocateOptionals.mjs`)随后将 SLM 可选依赖闭包共置到 - `dist/node_modules`,使 Worker 解析单一的 `@huggingface/transformers` 3.5.2 - 可选实例 — standalone trace 仅打包 transformers,不包含动态导入的 + `dist/node_modules`,使 Worker 解析单一的 `@huggingface/transformers` ^4.2.0 + 实例 — standalone trace 仅打包 transformers,不包含动态导入的 可选依赖,否则 Worker 会基于根目录的 transformers 加载 llmlingua-2, SLM 层将静默失效。 - [ ] `omniroute status` 在无 `.env` 的情况下正常工作(CLI Token 路径,仅 loopback) diff --git a/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md index bbadf03423..c604668d19 100644 --- a/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md @@ -322,14 +322,12 @@ npm run build:release - [ ] `npm install -g omniroute@<此版本>` 執行 postinstall 而不會致命退出 - [ ] 更新路徑保留選擇性依賴:`omniroute update --apply` 和自動更新器 執行 `npm install -g … --include=optional`,因此 `optionalDependencies`(better-sqlite3、 - keytar、tls-client,以及 llmlingua SLM 堆疊:`@atjsh/llmlingua-2`、 - `@huggingface/transformers@3.5.2`、`@tensorflow/tfjs`、`js-tiktoken`)在更新後仍會保留。 - `@huggingface/transformers` 維持選擇性,因此其 `onnxruntime-node` CUDA 提供者的 postinstall - 不會在 CUDA 11 主機上中斷安裝。Ultra `modelPath` SLM 層還需要 + keytar、tls-client,以及 llmlingua SLM 堆疊:`@atjsh/llmlingua-2@2.0.5`、 + `js-tiktoken`)在更新後仍會保留。Ultra `modelPath` SLM 層還需要 tinybert 模型,會在首次使用時自動下載到 `${DATA_DIR}/models/llmlingua`。Postinstall (`scripts/build/colocateOptionals.mjs`)接著將 SLM 選擇性閉包複製到 - `dist/node_modules`,使工作者解析到**單一** `@huggingface/transformers` 3.5.2 - 選擇性實例——獨立追蹤僅捆綁 transformers,而非動態匯入的 + `dist/node_modules`,使工作者解析到**單一** `@huggingface/transformers` ^4.2.0 + 實例——獨立追蹤僅捆綁 transformers,而非動態匯入的 選擇性套件,因此若無此步驟,工作者會載入 llmlingua-2 並使用根目錄的 transformers, 導致 SLM 層靜默地失敗但仍保持運作。 - [ ] `omniroute status` 在無 `.env` 的情況下正常運作(僅限 CLI 權杖路徑,迴環介面) diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md index 310bd5cf84..dfa96d53ee 100644 --- a/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/ops/RELEASE_CHECKLIST.md @@ -351,14 +351,12 @@ Before shipping any v3.8.x release, verify these additional items: - [ ] `npm install -g omniroute@` runs postinstall without fatal exit - [ ] Update path keeps optional deps: `omniroute update --apply` and the auto-updater run `npm install -g … --include=optional` so `optionalDependencies` (better-sqlite3, - keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2`, - `@huggingface/transformers@3.5.2`, `@tensorflow/tfjs`, `js-tiktoken`) survive an update. - `@huggingface/transformers` stays optional so its `onnxruntime-node` CUDA provider postinstall - cannot abort installation on CUDA 11 hosts. The ultra `modelPath` SLM tier also needs the + keytar, tls-client, and the llmlingua SLM stack: `@atjsh/llmlingua-2@2.0.5`, + `js-tiktoken`) survive an update. The ultra `modelPath` SLM tier also needs the tinybert model, auto-downloaded to `${DATA_DIR}/models/llmlingua` on first use. Postinstall (`scripts/build/colocateOptionals.mjs`) then co-locates the SLM optional closure into - `dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` 3.5.2 - optional instance — the standalone trace bundles only transformers, not the dynamically-imported + `dist/node_modules` so the worker resolves a SINGLE `@huggingface/transformers` ^4.2.0 + instance — the standalone trace bundles only transformers, not the dynamically-imported optionals, so without this the worker would load llmlingua-2 against the root's transformers and the SLM tier would silently fail-open. - [ ] `omniroute status` works with no `.env` (CLI token path, loopback only) diff --git a/open-sse/services/compression/engines/llmlingua/worker.ts b/open-sse/services/compression/engines/llmlingua/worker.ts index bf6fba4963..00ba3e1255 100644 --- a/open-sse/services/compression/engines/llmlingua/worker.ts +++ b/open-sse/services/compression/engines/llmlingua/worker.ts @@ -8,7 +8,7 @@ * * ## Fail-open paths * 1. Optional-deps gate: if any of `@atjsh/llmlingua-2`, `@huggingface/transformers`, - * `@tensorflow/tfjs`, `js-tiktoken` does not resolve, return `text` immediately — + * `js-tiktoken` does not resolve, return `text` immediately — * NO worker spawn. This is the default in CI / most installs (deps are OPTIONAL). * 2. Per-call timeout: first call for a model gets `FIRST_CALL_TIMEOUT_MS` (one-time * model load); warm calls get `LLMLINGUA_WORKER_TIMEOUT_MS`. On timeout → original @@ -16,7 +16,7 @@ * 3. Worker error/exit → resolve all pending with their original text + respawn next. * * ## Serialization - * ONNX/tfjs are not reentrant — calls are queued FIFO and only one message is + * ONNX inference is not reentrant — calls are queued FIFO and only one message is * in-flight at a time (the next is posted after the previous reply or its timeout). * * ## Idle eviction @@ -45,7 +45,7 @@ const FIRST_CALL_TIMEOUT_MS = 60000; /** * Gate probe: `@atjsh/llmlingua-2` is the entry package that declares the others - * (`@huggingface/transformers`, `@tensorflow/tfjs`, `js-tiktoken`) as peers. We probe + * (`@huggingface/transformers`, `js-tiktoken`) as peers. We probe * ONLY it (by manifest existence) because the peers are ESM-only and `require.resolve` * throws for them even when installed; the worker still fail-opens if a peer is * genuinely missing at `import()` time. diff --git a/package-lock.json b/package-lock.json index 2741e67b37..939fdf0071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,8 +155,7 @@ "node": ">=22.22.2 <23 || >=24.0.0 <27" }, "optionalDependencies": { - "@atjsh/llmlingua-2": "2.0.3", - "@tensorflow/tfjs": "4.22.0", + "@atjsh/llmlingua-2": "2.0.5", "better-sqlite3": "^13.0.2", "js-tiktoken": "^1.0.20", "keytar": "^7.9.0", @@ -556,17 +555,16 @@ } }, "node_modules/@atjsh/llmlingua-2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@atjsh/llmlingua-2/-/llmlingua-2-2.0.3.tgz", - "integrity": "sha512-UJJFMbzYldkZ4qX5CrSZtmytOnXf6aXhmr1sBhbpVMHdmQG+7GCnrx5rIwPSOmozXD9KiPv5nnV6pvzxdtHdYQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@atjsh/llmlingua-2/-/llmlingua-2-2.0.5.tgz", + "integrity": "sha512-cXdGUJgx0e2Sui5gYC8kapOhw1HAxwzh9IuYPdqyB+VlP6SL9imIfyB7I4GTCl/iG+BUxaOqSrLqWsWYvDZuVQ==", "license": "MIT", "optional": true, "dependencies": { "es-toolkit": "^1.38.0" }, "peerDependencies": { - "@huggingface/transformers": "*", - "@tensorflow/tfjs": "*", + "@huggingface/transformers": "^3.5.2 || ^4.0.0", "js-tiktoken": "*" } }, @@ -12241,241 +12239,6 @@ "tailwindcss": "4.3.3" } }, - "node_modules/@tensorflow/tfjs": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", - "integrity": "sha512-0TrIrXs6/b7FLhLVNmfh8Sah6JgjBPH4mZ8JGb7NU6WW+cx00qK5BcAZxw7NCzxj6N8MRAIfHq+oNbPUNG5VAg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.22.0", - "@tensorflow/tfjs-backend-webgl": "4.22.0", - "@tensorflow/tfjs-converter": "4.22.0", - "@tensorflow/tfjs-core": "4.22.0", - "@tensorflow/tfjs-data": "4.22.0", - "@tensorflow/tfjs-layers": "4.22.0", - "argparse": "^1.0.10", - "chalk": "^4.1.0", - "core-js": "3.29.1", - "regenerator-runtime": "^0.13.5", - "yargs": "^16.0.3" - }, - "bin": { - "tfjs-custom-module": "dist/tools/custom_module/cli.js" - } - }, - "node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.22.0.tgz", - "integrity": "sha512-1u0FmuLGuRAi8D2c3cocHTASGXOmHc/4OvoVDENJayjYkS119fcTcQf4iHrtLthWyDIPy3JiPhRrZQC9EwnhLw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", - "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.22.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs-converter": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", - "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", - "license": "Apache-2.0", - "optional": true, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs-core": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", - "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.7.0", - "@types/seedrandom": "^2.4.28", - "@webgpu/types": "0.1.38", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "license": "MIT", - "optional": true - }, - "node_modules/@tensorflow/tfjs-data": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.22.0.tgz", - "integrity": "sha512-dYmF3LihQIGvtgJrt382hSRH4S0QuAp2w1hXJI2+kOaEqo5HnUPG0k5KA6va+S1yUhx7UBToUKCBHeLHFQRV4w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@types/node-fetch": "^2.1.2", - "node-fetch": "~2.6.1", - "string_decoder": "^1.3.0" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0", - "seedrandom": "^3.0.5" - } - }, - "node_modules/@tensorflow/tfjs-layers": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.22.0.tgz", - "integrity": "sha512-lybPj4ZNj9iIAPUj7a8ZW1hg8KQGfqWLlCZDi9eM/oNKCCAgchiyzx8OrYoWmRrB+AM6VNEeIT+2gZKg5ReihA==", - "license": "Apache-2.0 AND MIT", - "optional": true, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.22.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "optional": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/@tensorflow/tfjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "optional": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tensorflow/tfjs/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, "node_modules/@testing-library/jest-dom": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", @@ -13022,13 +12785,6 @@ "@types/node": "*" } }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT", - "optional": true - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -13059,24 +12815,6 @@ "undici-types": "~8.3.0" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, - "node_modules/@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", - "license": "MIT", - "optional": true - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -13153,13 +12891,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/seedrandom": { - "version": "2.4.34", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", - "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==", - "license": "MIT", - "optional": true - }, "node_modules/@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -13978,13 +13709,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@webgpu/types": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", - "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/@xmldom/xmldom": { "version": "0.9.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", @@ -17208,18 +16932,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-js": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", - "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -26774,13 +26486,6 @@ "node": ">=0.1.90" } }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -29266,52 +28971,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", - "license": "MIT", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-forge": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", @@ -33071,13 +32730,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT", - "optional": true - }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -33916,7 +33568,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/selfsigned": { @@ -34761,13 +34413,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/sql.js": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.2.tgz", diff --git a/package.json b/package.json index 8e9db759ad..5dcd258e8b 100644 --- a/package.json +++ b/package.json @@ -342,8 +342,7 @@ "onnxruntime-node": "~1.24.3" }, "optionalDependencies": { - "@atjsh/llmlingua-2": "2.0.3", - "@tensorflow/tfjs": "4.22.0", + "@atjsh/llmlingua-2": "2.0.5", "better-sqlite3": "^13.0.2", "js-tiktoken": "^1.0.20", "keytar": "^7.9.0", diff --git a/scripts/build/colocate-standalone.mjs b/scripts/build/colocate-standalone.mjs index d0298893f7..0748cf6db7 100644 --- a/scripts/build/colocate-standalone.mjs +++ b/scripts/build/colocate-standalone.mjs @@ -6,7 +6,10 @@ * deployment runs `server.js` from that directory directly (not the assembled * `dist/` bundle). The standalone trace cannot see worker_threads entrypoints * resolved at runtime, including the required call-log artifact worker and the - * optional LLMLingua-2 worker. It also omits LLMLingua's optional dependencies. + * optional LLMLingua-2 worker (`open-sse/services/compression/engines/llmlingua/onnxWorker.js`, + * dynamically spawned via worker_threads — untraceable by webpack). It also omits + * LLMLingua's optional SLM deps (`@atjsh/llmlingua-2`, `js-tiktoken`) — they are + * optionalDependencies and are only installed at the ROOT `node_modules`. * * The call-log worker is required, so a bundle failure must fail the build. * LLMLingua remains fail-soft when its optional dependencies are absent. diff --git a/scripts/build/colocateOptionals.mjs b/scripts/build/colocateOptionals.mjs index 073a59fbed..0aa3f38fab 100644 --- a/scripts/build/colocateOptionals.mjs +++ b/scripts/build/colocateOptionals.mjs @@ -4,31 +4,32 @@ * OmniRoute — Co-locate the LLMLingua-2 optional dependency closure into the standalone bundle. * * The compression "ultra" SLM tier (PR #4257) runs `@atjsh/llmlingua-2` + - * `@huggingface/transformers` + `@tensorflow/tfjs` + `js-tiktoken` inside a worker thread + * `@huggingface/transformers` + `js-tiktoken` inside a worker thread * (`open-sse/services/compression/engines/llmlingua/onnxWorker.js`, shipped under `dist/`). These * are `optionalDependencies`: npm installs them into the ROOT `node_modules` on * `--include=optional`, but the Next.js standalone trace bundles ONLY `@huggingface/transformers` - * (3.5.2, pinned) into `dist/node_modules` — it does NOT trace the optional, dynamically-imported + * (4.2.0, pinned) into `dist/node_modules` — it does NOT trace the optional, dynamically-imported * SLM packages. * * ## Why this matters (the instance-split bug) * * The worker lives under `dist/`, so its `import("@huggingface/transformers")` resolves - * `dist/node_modules/@huggingface/transformers` (3.5.2) and the worker sets the model `cacheDir` + * `dist/node_modules/@huggingface/transformers` (4.2.0) and the worker sets the model `cacheDir` * on THAT instance's `env`. But its `import("@atjsh/llmlingua-2")` walks past `dist/node_modules` * (no `@atjsh` there) up to the ROOT `node_modules`, and llmlingua-2's own * `import("@huggingface/transformers")` then resolves the ROOT transformers — a DIFFERENT instance. * The `cacheDir`/`localModelPath` config the worker set never reaches the instance llmlingua-2 * actually uses, so the local model under `DATA_DIR/models/llmlingua` is never found and the SLM - * tier silently fails-open (no compression). Worse, if the root transformers is a 4.x line, - * llmlingua-2 throws on a tokenizer-API change (`decoder.decode` is undefined). + * tier silently fails-open (no compression). (Before `@atjsh/llmlingua-2@2.0.5` a root + * transformers on the 4.x line also made llmlingua-2 throw on a tokenizer-API change + * — `decoder.decode` is undefined; 2.0.5+ supports both v3 and v4.) * * ## The fix * * Co-locate the SLM optional dependency CLOSURE from the root `node_modules` into - * `dist/node_modules` (NO-CLOBBER, so the pinned `dist` transformers 3.5.2 / onnxruntime / sharp + * `dist/node_modules` (NO-CLOBBER, so the pinned `dist` transformers 4.2.0 / onnxruntime / sharp * stay). Then the worker resolves `@atjsh/llmlingua-2` AND `@huggingface/transformers` from the - * SAME `dist/node_modules` — a single 3.5.2 instance — so the env config applies and the local + * SAME `dist/node_modules` — a single 4.2.0 instance — so the env config applies and the local * model loads. * * `@huggingface/transformers` is intentionally NOT a closure seed: it is a PEER of @@ -54,7 +55,7 @@ import { dirname, join, sep } from "node:path"; * Entry packages of the SLM optional stack (the closure roots). `@huggingface/transformers` is * deliberately absent — it is the pinned instance already present in `dist/node_modules`. */ -export const SEED_PACKAGES = ["@atjsh/llmlingua-2", "@tensorflow/tfjs", "js-tiktoken"]; +export const SEED_PACKAGES = ["@atjsh/llmlingua-2", "js-tiktoken"]; /** * Compute the transitive dependency closure of `seeds` by walking each package's `dependencies` + diff --git a/scripts/build/prepublish.ts b/scripts/build/prepublish.ts index 340fcba946..b1e398deb0 100644 --- a/scripts/build/prepublish.ts +++ b/scripts/build/prepublish.ts @@ -402,7 +402,7 @@ runBuildTool( // The worker is spawned via worker_threads at a path the Next.js bundler cannot // statically trace, so it must ship as a standalone .js (mirrors the MCP-server // bundling above). Heavy deps (@atjsh/llmlingua-2 / @huggingface/transformers / -// @tensorflow/tfjs / js-tiktoken) stay EXTERNAL — they are optionalDependencies, +// js-tiktoken) stay EXTERNAL — they are optionalDependencies, // dynamically imported at runtime, and the worker fail-opens if any is absent. const llmWorkerSrc = join( ROOT, diff --git a/scripts/packs/optionalPackManifest.mjs b/scripts/packs/optionalPackManifest.mjs index c1e9495cae..f20fbc5df2 100644 --- a/scripts/packs/optionalPackManifest.mjs +++ b/scripts/packs/optionalPackManifest.mjs @@ -50,7 +50,6 @@ export const OPTIONAL_PACKS = [ { name: "@huggingface/transformers" }, { name: "onnxruntime-node" }, { name: "@atjsh/llmlingua-2" }, - { name: "@tensorflow/tfjs" }, { name: "js-tiktoken" }, ], }, @@ -156,7 +155,7 @@ export async function dirChecksum(dir) { hash.update(String(size)); hash.update("\0"); try { - // Stream to keep memory bounded on multi-hundred-MB packages (tfjs). + // Stream to keep memory bounded on multi-hundred-MB packages (onnxruntime-node). for await (const chunk of createReadStream(absolute)) hash.update(chunk); } catch { hash.update(""); diff --git a/tests/unit/colocate-optionals.test.ts b/tests/unit/colocate-optionals.test.ts index 711c405f99..8250c46317 100644 --- a/tests/unit/colocate-optionals.test.ts +++ b/tests/unit/colocate-optionals.test.ts @@ -29,10 +29,9 @@ function mkPkg( /** * Build a root tree mirroring the real SLM optional shape: - * @atjsh/llmlingua-2 → dep es-toolkit, PEER @huggingface/transformers (+ tfjs, js-tiktoken) - * @tensorflow/tfjs → dep @tensorflow/tfjs-core → dep long + * @atjsh/llmlingua-2 → dep es-toolkit, PEER @huggingface/transformers (+ js-tiktoken) * js-tiktoken → dep base64-js - * @huggingface/transformers present at root as a (stale) 4.2.0 + * @huggingface/transformers present at root as a (hypothetical future) 5.0.0 * * Each mock package gets a resolvable entrypoint so that isPackageIntact (which * checks entrypoint integrity via require.resolve) can validate the co-located @@ -49,26 +48,12 @@ function buildRoot(rootDir: string): void { dependencies: { "es-toolkit": "^1.38.0" }, peerDependencies: { "@huggingface/transformers": "*", - "@tensorflow/tfjs": "*", "js-tiktoken": "*", }, }, { "dist/index.js": "export const llmlingua = true;\n" } ); mkPkg(rootNm, "es-toolkit", { main: "index.js" }, { "index.js": "export const esToolkit = true;\n" }); - mkPkg( - rootNm, - "@tensorflow/tfjs", - { main: "index.js", dependencies: { "@tensorflow/tfjs-core": "4.22.0" } }, - { "index.js": "export const tfjs = true;\n" } - ); - mkPkg( - rootNm, - "@tensorflow/tfjs-core", - { main: "index.js", dependencies: { long: "^5.0.0" } }, - { "index.js": "export const tfjsCore = true;\n" } - ); - mkPkg(rootNm, "long", { main: "index.js" }, { "index.js": "export const long = true;\n" }); mkPkg( rootNm, "js-tiktoken", @@ -76,8 +61,8 @@ function buildRoot(rootDir: string): void { { "index.js": "export const tiktoken = true;\n" } ); mkPkg(rootNm, "base64-js", { main: "index.js" }, { "index.js": "export const base64 = true;\n" }); - // Root transformers is the STALE 4.x line — the bug we must not propagate into dist. - mkPkg(rootNm, "@huggingface/transformers", { version: "4.2.0" }); + // Root transformers is a hypothetical FUTURE line — the version we must not propagate into dist. + mkPkg(rootNm, "@huggingface/transformers", { version: "5.0.0" }); } test("computeDependencyClosure walks deps transitively and skips peers (transformers)", () => { @@ -88,11 +73,8 @@ test("computeDependencyClosure walks deps transitively and skips peers (transfor for (const expected of [ "@atjsh/llmlingua-2", - "@tensorflow/tfjs", "js-tiktoken", "es-toolkit", - "@tensorflow/tfjs-core", - "long", "base64-js", ]) { assert.ok(closure.includes(expected), `closure should include ${expected}`); @@ -111,23 +93,20 @@ test("colocateLlmlinguaOptionals copies the closure into dist and never clobbers const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-copy-")); try { buildRoot(root); - // dist already ships the PINNED transformers (3.5.2) — must survive untouched. + // dist already ships the PINNED transformers (4.2.0) — must survive untouched. const distNm = join(root, "dist", "node_modules"); - mkPkg(distNm, "@huggingface/transformers", { version: "3.5.2" }); + mkPkg(distNm, "@huggingface/transformers", { version: "4.2.0" }); const result = colocateLlmlinguaOptionals({ rootDir: root }); assert.equal(result.skipped, false); if (result.skipped === false) { - assert.ok(result.copied >= 6, `expected >=6 packages copied, got ${result.copied}`); + assert.ok(result.copied >= 4, `expected >=4 packages copied, got ${result.copied}`); } // Full closure landed in dist/node_modules. for (const name of [ "@atjsh/llmlingua-2", "es-toolkit", - "@tensorflow/tfjs", - "@tensorflow/tfjs-core", - "long", "js-tiktoken", "base64-js", ]) { @@ -136,11 +115,11 @@ test("colocateLlmlinguaOptionals copies the closure into dist and never clobbers // The package payload came along (not just the manifest). assert.ok(existsSync(join(distNm, "@atjsh", "llmlingua-2", "dist", "index.js"))); - // CRITICAL: dist's pinned transformers is preserved — root's 4.2.0 must NOT win. + // CRITICAL: dist's pinned transformers is preserved — root's 5.0.0 must NOT win. const distTransformers = JSON.parse( readFileSync(join(distNm, "@huggingface", "transformers", "package.json"), "utf8") ); - assert.equal(distTransformers.version, "3.5.2", "dist transformers must remain 3.5.2"); + assert.equal(distTransformers.version, "4.2.0", "dist transformers must remain 4.2.0"); } finally { rmSync(root, { recursive: true, force: true }); } @@ -150,7 +129,7 @@ test("colocateLlmlinguaOptionals is idempotent (second run is a no-op)", () => { const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-idem-")); try { buildRoot(root); - mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "3.5.2" }); + mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "4.2.0" }); const first = colocateLlmlinguaOptionals({ rootDir: root }); assert.equal(first.skipped, false); @@ -169,7 +148,7 @@ test("colocateLlmlinguaOptionals skips when SLM optionals are not installed", () const root = mkdtempSync(join(tmpdir(), "omniroute-colocate-noopt-")); try { // dist bundle exists, but the optional seeds were never installed at root. - mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "3.5.2" }); + mkPkg(join(root, "dist", "node_modules"), "@huggingface/transformers", { version: "4.2.0" }); mkdirSync(join(root, "node_modules"), { recursive: true }); const result = colocateLlmlinguaOptionals({ rootDir: root }); @@ -208,7 +187,7 @@ test("colocateLlmlinguaOptionals fills a Next-traced stub (package.json only, no try { buildRoot(root); const distNm = join(root, "dist", "node_modules"); - mkPkg(distNm, "@huggingface/transformers", { version: "3.5.2" }); + mkPkg(distNm, "@huggingface/transformers", { version: "4.2.0" }); // Simulate the Next-traced stub: directory exists, package.json only. const stubDir = join(distNm, "@atjsh", "llmlingua-2"); @@ -233,5 +212,5 @@ test("colocateLlmlinguaOptionals fills a Next-traced stub (package.json only, no test("SEED_PACKAGES excludes transformers (it is a dist-pinned peer, not a seed)", () => { assert.ok(!SEED_PACKAGES.includes("@huggingface/transformers")); - assert.deepEqual(SEED_PACKAGES, ["@atjsh/llmlingua-2", "@tensorflow/tfjs", "js-tiktoken"]); + assert.deepEqual(SEED_PACKAGES, ["@atjsh/llmlingua-2", "js-tiktoken"]); }); diff --git a/tests/unit/compression/llmlingua-worker.test.ts b/tests/unit/compression/llmlingua-worker.test.ts index aea8931da6..7bb74f2288 100644 --- a/tests/unit/compression/llmlingua-worker.test.ts +++ b/tests/unit/compression/llmlingua-worker.test.ts @@ -1,8 +1,8 @@ /** * Tests for the real LLMLingua worker-thread backend (`worker.ts` + `onnxWorker.ts`). * - * The four optional deps (`@atjsh/llmlingua-2`, `@huggingface/transformers`, - * `@tensorflow/tfjs`, `js-tiktoken`) are NOT installed in this worktree, so the + * The three optional deps (`@atjsh/llmlingua-2`, `@huggingface/transformers`, + * `js-tiktoken`) are NOT installed in this worktree, so the * default path MUST fail-open WITHOUT spawning a worker: * * 1. Deps absent → fail-open, no spawn (ALWAYS runs here): the backend returns the @@ -23,12 +23,11 @@ import { const require = createRequire(import.meta.url); -/** Whether all four optional deps resolve in this environment. */ +/** Whether all three optional deps resolve in this environment. */ function depsResolve(): boolean { try { require.resolve("@atjsh/llmlingua-2"); require.resolve("@huggingface/transformers"); - require.resolve("@tensorflow/tfjs"); require.resolve("js-tiktoken"); return true; } catch { diff --git a/tests/unit/docker-llmlingua-optionals-9166.test.ts b/tests/unit/docker-llmlingua-optionals-9166.test.ts index da994ed5c4..ac57d0d362 100644 --- a/tests/unit/docker-llmlingua-optionals-9166.test.ts +++ b/tests/unit/docker-llmlingua-optionals-9166.test.ts @@ -16,7 +16,6 @@ import { assembleStandalone } from "../../scripts/build/assembleStandalone.mjs"; const REQUIRED_RUNTIME_PACKAGES = [ "@atjsh/llmlingua-2", "@huggingface/transformers", - "@tensorflow/tfjs", "js-tiktoken", ]; @@ -47,7 +46,7 @@ function mkPkg( function buildLlmlinguaRoot( rootDir: string, - transformersVersion = "3.5.2" + transformersVersion = "4.2.0" ): void { const rootNm = join(rootDir, "node_modules"); @@ -61,7 +60,6 @@ function buildLlmlinguaRoot( }, peerDependencies: { "@huggingface/transformers": "*", - "@tensorflow/tfjs": "*", "js-tiktoken": "*", }, }, @@ -72,18 +70,6 @@ function buildLlmlinguaRoot( mkPkg(rootNm, "es-toolkit"); - mkPkg(rootNm, "@tensorflow/tfjs", { - dependencies: { - "@tensorflow/tfjs-core": "4.22.0", - }, - }); - mkPkg(rootNm, "@tensorflow/tfjs-core", { - dependencies: { - long: "^5.0.0", - }, - }); - mkPkg(rootNm, "long"); - mkPkg(rootNm, "js-tiktoken", { dependencies: { "base64-js": "^1.5.1", @@ -138,8 +124,6 @@ test("#9166 standalone assembly includes the complete LLMLingua runtime closure" for (const packageName of [ ...REQUIRED_RUNTIME_PACKAGES, "es-toolkit", - "@tensorflow/tfjs-core", - "long", "base64-js", "onnxruntime-node", ]) { @@ -175,14 +159,14 @@ test("#9166 standalone assembly never overwrites an already pinned transformers ); try { - buildLlmlinguaRoot(root, "4.2.0"); + buildLlmlinguaRoot(root, "5.0.0"); const { distDir, standaloneDir } = createStandalone(root); mkPkg( join(standaloneDir, "node_modules"), "@huggingface/transformers", { - version: "3.5.2", + version: "4.2.0", } ); @@ -208,7 +192,7 @@ test("#9166 standalone assembly never overwrites an already pinned transformers assert.equal( targetManifest.version, - "3.5.2", + "4.2.0", "standalone's pinned transformers version must not be overwritten" ); @@ -286,9 +270,6 @@ test("#9166 co-location is not skipped when every closure dir exists but one is // llmlingua-2 one is the partial NFT-trace shell without its main. for (const packageName of [ "es-toolkit", - "@tensorflow/tfjs", - "@tensorflow/tfjs-core", - "long", "js-tiktoken", "base64-js", "@huggingface/transformers", From f71f3da08bcc3b1585f21f4e772a0b2d41e73516 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 20 Aug 2026 22:04:03 -0300 Subject: [PATCH 130/135] feat(routing): add DISABLE_CONTEXT_WINDOW_CHECKS bypass for the direct-request input/context check (#10927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rescoped from #10606 — see PR body for the full rationale (combo-routing half made moot by #10162's advisory-only architecture, chatCore.ts hard-reject bypass retains real value). Validated in an isolated worktree boarded onto origin/release/v3.8.50: - 65/65 focused unit tests pass (chatcore-model-output-cap-wiring + feature-flags-settings). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: JxnLexn <10897478+JxnLexn@users.noreply.github.com> --- .env.example | 9 ++++ .../features/disable-context-window-checks.md | 1 + docs/reference/ENVIRONMENT.md | 1 + docs/reference/FEATURE_FLAGS.md | 3 +- open-sse/handlers/chatCore.ts | 14 +++++-- src/i18n/messages/de.json | 5 +++ src/i18n/messages/en.json | 5 +++ src/i18n/messages/pt-BR.json | 5 +++ src/i18n/messages/vi.json | 5 +++ .../constants/featureFlagDefinitions.ts | 14 ++++++- src/shared/utils/featureFlags.ts | 16 ++++++++ .../chatcore-model-output-cap-wiring.test.ts | 30 ++++++++++++++ tests/unit/feature-flags-settings.test.ts | 41 ++++++++++++++++++- 13 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 changelog.d/features/disable-context-window-checks.md diff --git a/.env.example b/.env.example index 899ba5b0e7..698c09329b 100644 --- a/.env.example +++ b/.env.example @@ -417,6 +417,15 @@ ALLOW_API_KEY_REVEAL=false # by OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT and the heap-pressure shed instead. Set a positive # value only on memory-constrained deployments that need a hard ceiling. # OMNIROUTE_CHAT_HARD_MAX_MESSAGES=0 + +# Skip OmniRoute's local context-window and max-input-token check for direct +# single-model requests. Default: false (dangerous opt-in). +# The upstream provider still enforces its real limits, so enabling this can +# replace an early OmniRoute 400 with an upstream context-length error. +# Prompt compression and the model's own output-token cap remain active. +# Also configurable from Dashboard > Settings > Feature Flags; no restart is +# required. Used by: src/shared/utils/featureFlags.ts and open-sse/handlers/chatCore.ts. +# DISABLE_CONTEXT_WINDOW_CHECKS=false # How long a heavy request waits for heavyweight capacity before a retryable 503. # A short bounded wait serializes agent bursts instead of an instant 503; 0 = instant. # Default 2000 (2s). diff --git a/changelog.d/features/disable-context-window-checks.md b/changelog.d/features/disable-context-window-checks.md new file mode 100644 index 0000000000..1cdd3cc0a8 --- /dev/null +++ b/changelog.d/features/disable-context-window-checks.md @@ -0,0 +1 @@ +- feat(routing): add the default-off `DISABLE_CONTEXT_WINDOW_CHECKS` feature flag to let operators bypass OmniRoute's local context-window and max-input-token check for direct single-model requests, leaving upstream limits, prompt compression, and output-token caps intact. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 1104618012..c475f2cadf 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -282,6 +282,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp | `OMNIROUTE_PAYLOAD_RULES_RELOAD_MS` | `5000` | `open-sse/services/payloadRules.ts` | Reload interval (ms) for hot-reloading the payload rules file. Minimum `1000`. | | `OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS` | `false` | `open-sse/services/model.ts` | Opt-in: route bare `claude-*` model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Explicit provider prefixes still win. Also configurable via a dashboard toggle on the Claude provider page. | | `COMBO_CONCURRENCY_PER_MODEL` | `3` | `open-sse/services/comboConfig.ts` | Per-model concurrency cap for round-robin combos (#9100). The round-robin combo semaphore was hard-capped at 3 concurrent requests per model with no override, serializing higher-concurrency traffic behind that cap. Validated to `>= 1`, clamped to `<= 32`. | +| `DISABLE_CONTEXT_WINDOW_CHECKS` | `false` | `open-sse/handlers/chatCore.ts` | Dangerous opt-in that skips OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits; prompt compression and the model's own output-token cap remain active. Effective precedence is Feature Flags DB override > environment variable > default; no restart is required. | --- diff --git a/docs/reference/FEATURE_FLAGS.md b/docs/reference/FEATURE_FLAGS.md index 45eededb28..8b46db649b 100644 --- a/docs/reference/FEATURE_FLAGS.md +++ b/docs/reference/FEATURE_FLAGS.md @@ -76,13 +76,14 @@ used when neither a DB override nor an environment variable is present. | `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` | boolean | `true` | | Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN). On by default (local-first); disable for strict public-only blocking. Cloud-metadata stays blocked. | | `ENABLE_CC_COMPATIBLE_PROVIDER` | boolean | `false` | ✓ | Enable Claude Code compatible provider mode. | -### Policies (3) +### Policies (4) | Key | Type | Default | Restart | Description | | ----------------------------------------- | ------- | ---------- | ------- | ---------------------------------------------------------------------- | | `TOOL_POLICY_MODE` | enum | `disabled` | | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. | | `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | | Automatically enable rate limiting based on usage patterns. | | `ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE` | boolean | `false` | ✓ | Allow multiple connections per compatibility node. | +| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. | ### Runtime (11) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index c40bca8296..dcc281d3e3 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -202,7 +202,10 @@ import { deriveRequestCapabilityRequirements, buildCapabilityMismatchMessage, } from "@/shared/constants/capabilities/capabilityFilter.ts"; -import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags.ts"; +import { + areContextWindowChecksDisabled, + isFeatureFlagEnabled, +} from "@/shared/utils/featureFlags.ts"; import { resolveNoAuthEchoModel } from "./chatCore/noAuthEchoModel.ts"; import { REASONING_BUFFER_MIN_TRIGGER, @@ -2041,13 +2044,18 @@ export async function handleChatCore({ const modelOutputCap = toPositiveInteger( getExplicitModelOutputCap({ provider, model: effectiveModel }) ); + const contextWindowChecksDisabled = areContextWindowChecksDisabled(); const outputBudget = enforceOutputTokenBudget( body as Record, finalEstimatedInputTokens, - finalContextLimit, + contextWindowChecksDisabled ? Number.MAX_SAFE_INTEGER : finalContextLimit, targetFormat === FORMATS.CLAUDE && sourceFormat !== FORMATS.CLAUDE ? DEFAULT_MAX_TOKENS : 0, modelOutputCap, - toPositiveInteger(resolveInputTokenCapForGate({ provider, model: effectiveModel }, { isCombo })) + contextWindowChecksDisabled + ? null + : toPositiveInteger( + resolveInputTokenCapForGate({ provider, model: effectiveModel }, { isCombo }) + ) ); if (outputBudget.ok === false) { const exceededInputCap = outputBudget.maxInputTokens !== undefined; diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index fb4e14b485..c1ffc929ea 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -12716,6 +12716,10 @@ "ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE": { "description": "Mehrere Verbindungen für jeden Kompatibilitätsknoten zulassen." }, + "DISABLE_CONTEXT_WINDOW_CHECKS": { + "label": "Kontextfensterprüfungen deaktivieren", + "description": "Lokale Kontextfenster- und Maximaleingabetoken-Prüfung von OmniRoute für direkte Einzelmodell-Anfragen überspringen. Upstream-Anbieter erzwingen weiterhin ihre tatsächlichen Grenzen. Prompt-Komprimierung und Ausgabetoken-Grenzen bleiben aktiv." + }, "RESPONSES_PASSTHROUGH_DROP_COMMENTARY": { "description": "Interne Ausgabeelemente der Kommentarphase aus den Passthrough-Streams der Responses-API entfernen, bevor sie an Clients weitergeleitet werden. Deaktivieren Sie dieses Flag, um rohe Upstream-Kommentare zu erhalten." }, @@ -13356,6 +13360,7 @@ } }, "featureFlagCapabilityFilterEnabledDescription": "Lehnen Sie Anfragen ab, bevor sie versendet werden, wenn das Zielmodell über die erforderlichen Funktionen (Vision, Werkzeuge, strukturierte Ausgabe, Kontextfenster) nicht verfügt. Schützt direkte Einzelanbieteranfragen, die den Kombo-Schicht-Kompatibilitätsfilter umgehen.", + "featureFlagDisableContextWindowChecksDescription": "Lokale Kontextfenster- und Maximaleingabetoken-Prüfung von OmniRoute für direkte Einzelmodell-Anfragen überspringen. Upstream-Anbieter erzwingen weiterhin ihre tatsächlichen Grenzen. Prompt-Komprimierung und Ausgabetoken-Grenzen bleiben aktiv.", "publicSystem": { "notFound": { "title": "Seite nicht gefunden", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index bdccf17e23..aab9e6eddc 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -12754,6 +12754,10 @@ "ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE": { "description": "Allow multiple connections for each compatibility node." }, + "DISABLE_CONTEXT_WINDOW_CHECKS": { + "label": "Disable Context Window Checks", + "description": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active." + }, "RESPONSES_PASSTHROUGH_DROP_COMMENTARY": { "description": "Remove internal commentary-phase output items from Responses API passthrough streams before forwarding them to clients. Disable this flag to receive raw upstream commentary." }, @@ -13394,6 +13398,7 @@ } }, "featureFlagCapabilityFilterEnabledDescription": "Reject requests before dispatch when the target model lacks required capabilities (vision, tools, structured output, context window). Protects direct single-provider requests that bypass the combo-layer compatibility filter.", + "featureFlagDisableContextWindowChecksDescription": "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers still enforce their actual limits. Prompt compression and output-token caps remain active.", "publicSystem": { "notFound": { "title": "Page not found", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index a4f8a5f3cf..b3fe3125dd 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -12747,6 +12747,10 @@ "ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE": { "description": "Permite múltiplas conexões para cada nó de compatibilidade." }, + "DISABLE_CONTEXT_WINDOW_CHECKS": { + "label": "Desativar verificações da janela de contexto", + "description": "Ignora a verificação local do OmniRoute para janela de contexto e limite máximo de tokens de entrada em solicitações diretas a um único modelo. Os provedores upstream continuam aplicando seus limites reais. A compactação de prompts e os limites de tokens de saída permanecem ativos." + }, "RESPONSES_PASSTHROUGH_DROP_COMMENTARY": { "description": "Remove itens de saída da fase de comentário interno dos streams de passthrough da Responses API antes de encaminhá-los aos clientes. Desative esta flag para receber o comentário bruto do upstream." }, @@ -13387,6 +13391,7 @@ } }, "featureFlagCapabilityFilterEnabledDescription": "Rejeitar requisições antes do despacho quando o modelo alvo nao possui as capacidades necessarias (visao, ferramentas, saída estruturada, janela de contexto). Protege requisições diretas que ignoram o filtro de compatibilidade do combo.", + "featureFlagDisableContextWindowChecksDescription": "Ignora a verificação local do OmniRoute para janela de contexto e limite máximo de tokens de entrada em solicitações diretas a um único modelo. Os provedores upstream continuam aplicando seus limites reais. A compactação de prompts e os limites de tokens de saída permanecem ativos.", "publicSystem": { "notFound": { "title": "Página não encontrada", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index eb1cb41b6e..b4ba071acd 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -12755,6 +12755,10 @@ "ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE": { "description": "Cho phép nhiều kết nối trên mỗi node tương thích." }, + "DISABLE_CONTEXT_WINDOW_CHECKS": { + "label": "Tắt kiểm tra cửa sổ ngữ cảnh", + "description": "Bỏ qua kiểm tra cục bộ của OmniRoute về cửa sổ ngữ cảnh và giới hạn token đầu vào tối đa cho yêu cầu trực tiếp đến một mô hình đơn lẻ. Nhà cung cấp thượng nguồn vẫn áp dụng các giới hạn thực tế. Tính năng nén prompt và giới hạn token đầu ra vẫn hoạt động." + }, "RESPONSES_PASSTHROUGH_DROP_COMMENTARY": { "description": "Loại các mục đầu ra thuộc giai đoạn commentary nội bộ khỏi luồng chuyển tiếp Responses API trước khi gửi tới ứng dụng khách. Tắt cờ này để nhận nguyên dữ liệu commentary từ thượng nguồn." }, @@ -13395,6 +13399,7 @@ } }, "featureFlagCapabilityFilterEnabledDescription": "Từ chối yêu cầu trước khi gửi đi khi mô hình đích thiếu các khả năng bắt buộc (thị giác, công cụ, đầu ra có cấu trúc, cửa sổ ngữ cảnh). Bảo vệ các yêu cầu trực tiếp đến một nhà cung cấp khi chúng bỏ qua bộ lọc tương thích của combo.", + "featureFlagDisableContextWindowChecksDescription": "Bỏ qua kiểm tra cục bộ của OmniRoute về cửa sổ ngữ cảnh và giới hạn token đầu vào tối đa cho yêu cầu trực tiếp đến một mô hình đơn lẻ. Nhà cung cấp thượng nguồn vẫn áp dụng các giới hạn thực tế. Tính năng nén prompt và giới hạn token đầu ra vẫn hoạt động.", "publicSystem": { "notFound": { "title": "Không tìm thấy trang", diff --git a/src/shared/constants/featureFlagDefinitions.ts b/src/shared/constants/featureFlagDefinitions.ts index 98baeafbec..37861d2994 100644 --- a/src/shared/constants/featureFlagDefinitions.ts +++ b/src/shared/constants/featureFlagDefinitions.ts @@ -236,7 +236,7 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [ warningLevel: "info", }, - // ──────────────── Policies (4) ──────────────── + // ──────────────── Policies (5) ──────────────── { key: "TOOL_POLICY_MODE", label: "Tool Policy Mode", @@ -271,6 +271,18 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [ requiresRestart: true, warningLevel: "info", }, + { + key: "DISABLE_CONTEXT_WINDOW_CHECKS", + label: "Disable Context Window Checks", + description: + "Skip OmniRoute's local context-window and max-input-token check for direct single-model requests. Upstream providers remain responsible for enforcing their actual limits. Off by default.", + descriptionI18nKey: "featureFlagDisableContextWindowChecksDescription", + category: "policies", + defaultValue: "false", + type: "boolean", + requiresRestart: false, + warningLevel: "danger", + }, { key: "CAPABILITY_FILTER_ENABLED", label: "Capability Filter", diff --git a/src/shared/utils/featureFlags.ts b/src/shared/utils/featureFlags.ts index 9a3582370d..54874fcbff 100644 --- a/src/shared/utils/featureFlags.ts +++ b/src/shared/utils/featureFlags.ts @@ -72,6 +72,22 @@ export function isCcCompatibleProviderEnabled(): boolean { return isFeatureFlagEnabled("ENABLE_CC_COMPATIBLE_PROVIDER"); } +/** + * Context-window checks are fail-safe: an unavailable flag store must never + * silently disable local request bounds. + */ +export function areContextWindowChecksDisabled(): boolean { + try { + return isFeatureFlagEnabled("DISABLE_CONTEXT_WINDOW_CHECKS"); + } catch (error) { + console.error( + "[featureFlags] Failed to resolve DISABLE_CONTEXT_WINDOW_CHECKS, keeping checks enabled:", + error instanceof Error ? error.message : error + ); + return false; + } +} + export function isApiKeyRevealEnabledFlag(): boolean { try { return isFeatureFlagEnabled("ALLOW_API_KEY_REVEAL"); diff --git a/tests/unit/chatcore-model-output-cap-wiring.test.ts b/tests/unit/chatcore-model-output-cap-wiring.test.ts index 0331aeda67..9cc0042a3e 100644 --- a/tests/unit/chatcore-model-output-cap-wiring.test.ts +++ b/tests/unit/chatcore-model-output-cap-wiring.test.ts @@ -12,6 +12,7 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const overridesDb = await import("../../src/lib/db/modelCapabilityOverrides.ts"); +const featureFlagsDb = await import("../../src/lib/db/featureFlags.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); const PROVIDER = "capwire-testprov"; @@ -77,6 +78,7 @@ test.before(() => { test.after(() => { globalThis.fetch = originalFetch; + featureFlagsDb.removeFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS"); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); @@ -114,3 +116,31 @@ test("handleChatCore dispatches input within the model input cap", async () => { assert.equal(fetchCalls, 1); assert.ok(dispatchedBody, "input below the cap must reach the upstream"); }); + +test("DISABLE_CONTEXT_WINDOW_CHECKS lets direct-model input exceed the declared input cap", async () => { + dispatchedBody = null; + fetchCalls = 0; + featureFlagsDb.setFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS", "true"); + try { + const result = await handleChatCore(buildRequest(1, "x".repeat(200))); + assert.equal(result.success, true); + assert.equal(fetchCalls, 1, "disabled context checks must let the upstream decide"); + assert.ok(dispatchedBody, "oversized input must reach the upstream when the flag is enabled"); + } finally { + featureFlagsDb.removeFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS"); + } +}); + +test("DISABLE_CONTEXT_WINDOW_CHECKS keeps the direct model output cap active", async () => { + dispatchedBody = null; + fetchCalls = 0; + featureFlagsDb.setFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS", "true"); + try { + const result = await handleChatCore(buildRequest(REQUESTED_MAX_TOKENS, "x".repeat(200))); + assert.equal(result.success, true); + assert.equal(fetchCalls, 1); + assert.equal(dispatchedBody?.max_tokens, OUTPUT_CAP); + } finally { + featureFlagsDb.removeFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS"); + } +}); diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index ac75c52bfe..f26fe182de 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -28,9 +28,10 @@ const { isModelCatalogNamesEnabled, isArenaEloSyncEnabled, isControlPlaneProxyDirectFallbackEnabled, + areContextWindowChecksDisabled, } = await import("../../src/shared/utils/featureFlags.ts"); -const EXPECTED_FEATURE_FLAG_COUNT = 50; +const EXPECTED_FEATURE_FLAG_COUNT = 51; // ────────────────────────────────────────────────────── // Test group 1 — Flag definitions registry @@ -207,6 +208,16 @@ describe("featureFlagDefinitions", () => { assert.strictEqual(def.warningLevel, "caution"); } }); + + it("defines context-window check bypass as a dangerous opt-in policy flag", () => { + const def = FEATURE_FLAG_DEFINITIONS.find((d) => d.key === "DISABLE_CONTEXT_WINDOW_CHECKS"); + assert.ok(def, "DISABLE_CONTEXT_WINDOW_CHECKS should exist"); + assert.strictEqual(def.category, "policies"); + assert.strictEqual(def.type, "boolean"); + assert.strictEqual(def.defaultValue, "false"); + assert.strictEqual(def.requiresRestart, false); + assert.strictEqual(def.warningLevel, "danger"); + }); }); // ────────────────────────────────────────────────────── @@ -429,6 +440,34 @@ describe("resolveFeatureFlag", () => { removeFeatureFlagOverride("OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK"); } }); + + it("areContextWindowChecksDisabled defaults off and follows DB overrides", () => { + assert.strictEqual(areContextWindowChecksDisabled(), false); + try { + setFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS", "true"); + assert.strictEqual(areContextWindowChecksDisabled(), true); + } finally { + removeFeatureFlagOverride("DISABLE_CONTEXT_WINDOW_CHECKS"); + } + }); + + it("areContextWindowChecksDisabled keeps checks enabled when the flag store is unreadable", () => { + const originalError = console.error; + console.error = () => {}; + try { + core.resetDbInstance(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.mkdirSync(tmpDir, { recursive: true }); + const blockerPath = path.join(tmpDir, "storage.sqlite"); + fs.mkdirSync(blockerPath, { recursive: true }); + assert.strictEqual(areContextWindowChecksDisabled(), false); + } finally { + console.error = originalError; + core.resetDbInstance(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + fs.mkdirSync(tmpDir, { recursive: true }); + } + }); }); }); From 2cd14b1696395f52edb06fb0b50996cd05b70035 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Fri, 21 Aug 2026 03:15:48 +0200 Subject: [PATCH 131/135] fix(providers): rename Freepik slug to Magnific and validate Magnific API keys (#10594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canonical provider id renamed freepik → magnific (Magnific Mystic official API), with a permanent redirect + runtime alias so old freepik/ traffic and /dashboard/providers/freepik URLs keep working. Existing provider=freepik connection rows are rewritten to magnific by migration 160. Closes #10604. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 138 files): - Focused suite: 54/54 tests pass (magnific-image-handler, provider-validation-image-only, provider-alias-uniqueness, redirects-cli-renames). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: RaviTharuma --- .env.example | 26 ++ AGENTS.md | 2 +- README.md | 6 +- .../fixes/10594-freepik-magnific-api.md | 1 + .../quality/open-sse-typecheck-baseline.json | 18 -- docs/diagrams/cli-terminal.svg | 10 +- docs/diagrams/comparison-table.svg | 2 +- docs/diagrams/promise-pillars.svg | 6 +- docs/diagrams/readme-hero.svg | 4 +- docs/guides/MANAGEMENT-AUTH.md | 8 +- docs/i18n/ar/llm.txt | 12 +- docs/i18n/az/llm.txt | 12 +- docs/i18n/bg/llm.txt | 12 +- docs/i18n/bn/llm.txt | 12 +- docs/i18n/cs/llm.txt | 12 +- docs/i18n/da/llm.txt | 12 +- docs/i18n/de/llm.txt | 12 +- docs/i18n/es/llm.txt | 12 +- docs/i18n/fa/llm.txt | 12 +- docs/i18n/fi/llm.txt | 12 +- docs/i18n/fr/llm.txt | 12 +- docs/i18n/gu/llm.txt | 12 +- docs/i18n/he/llm.txt | 12 +- docs/i18n/hi/llm.txt | 12 +- docs/i18n/hu/llm.txt | 12 +- docs/i18n/id/llm.txt | 12 +- docs/i18n/in/llm.txt | 12 +- docs/i18n/it/llm.txt | 12 +- docs/i18n/ja/llm.txt | 12 +- docs/i18n/ko/llm.txt | 12 +- docs/i18n/mr/llm.txt | 12 +- docs/i18n/ms/llm.txt | 12 +- docs/i18n/nl/llm.txt | 12 +- docs/i18n/no/llm.txt | 12 +- docs/i18n/phi/llm.txt | 12 +- docs/i18n/pl/llm.txt | 12 +- docs/i18n/pt-BR/llm.txt | 12 +- docs/i18n/pt/llm.txt | 12 +- docs/i18n/ro/llm.txt | 12 +- docs/i18n/ru/llm.txt | 12 +- docs/i18n/sk/llm.txt | 12 +- docs/i18n/sv/llm.txt | 12 +- docs/i18n/sw/llm.txt | 12 +- docs/i18n/ta/llm.txt | 12 +- docs/i18n/te/llm.txt | 12 +- docs/i18n/th/llm.txt | 12 +- docs/i18n/tr/llm.txt | 12 +- docs/i18n/uk-UA/llm.txt | 12 +- docs/i18n/ur/llm.txt | 12 +- docs/i18n/vi/llm.txt | 12 +- docs/i18n/zh-CN/llm.txt | 12 +- docs/i18n/zh-TW/llm.txt | 12 +- docs/reference/ENVIRONMENT.md | 6 + llm.txt | 4 +- next.config.mjs | 5 + open-sse/config/imageRegistry.ts | 18 +- .../providers/registry/freepik/index.ts | 26 -- .../providers/registry/magnific/index.ts | 26 ++ open-sse/executors/copilot-m365-connection.ts | 3 +- open-sse/executors/copilot-m365-web.ts | 7 +- open-sse/executors/index.ts | 3 +- open-sse/handlers/imageGeneration.ts | 6 +- .../providers/{freepik.ts => magnific.ts} | 94 ++++--- .../__tests__/glmCodingProviderConfig.test.ts | 3 + open-sse/mcp-server/fetchTimeout.ts | 22 +- open-sse/translator/helpers/toolCallShim.ts | 14 +- .../translator/response/claude-to-openai.ts | 34 +++ open-sse/utils/publicCreds.ts | 6 + open-sse/utils/usageTracking.ts | 8 + skills/omni-combos-routing/SKILL.md | 22 ++ skills/omni-inference/SKILL.md | 4 +- src/app/api/providers/route.ts | 4 +- src/app/api/v1/models/catalog.ts | 12 +- src/app/api/v1/responses/route.ts | 5 +- src/i18n/messages/ar.json | 199 ++++++++------- src/i18n/messages/az.json | 199 ++++++++------- src/i18n/messages/bg.json | 199 ++++++++------- src/i18n/messages/bn.json | 199 ++++++++------- src/i18n/messages/cs.json | 199 ++++++++------- src/i18n/messages/da.json | 199 ++++++++------- src/i18n/messages/de.json | 197 +++++++++------ src/i18n/messages/en.json | 2 +- src/i18n/messages/es.json | 199 ++++++++------- src/i18n/messages/fa.json | 199 ++++++++------- src/i18n/messages/fi.json | 199 ++++++++------- src/i18n/messages/fr.json | 197 +++++++++------ src/i18n/messages/gu.json | 199 ++++++++------- src/i18n/messages/he.json | 199 ++++++++------- src/i18n/messages/hi.json | 199 ++++++++------- src/i18n/messages/hu.json | 197 +++++++++------ src/i18n/messages/id.json | 199 ++++++++------- src/i18n/messages/in.json | 199 ++++++++------- src/i18n/messages/it.json | 195 +++++++++------ src/i18n/messages/ja.json | 199 ++++++++------- src/i18n/messages/ko.json | 199 ++++++++------- src/i18n/messages/mr.json | 199 ++++++++------- src/i18n/messages/ms.json | 199 ++++++++------- src/i18n/messages/nl.json | 197 +++++++++------ src/i18n/messages/no.json | 199 ++++++++------- src/i18n/messages/phi.json | 195 +++++++++------ src/i18n/messages/pl.json | 199 ++++++++------- src/i18n/messages/pt-BR.json | 107 ++++---- src/i18n/messages/pt.json | 201 ++++++++------- src/i18n/messages/ro.json | 199 ++++++++------- src/i18n/messages/ru.json | 193 +++++++++------ src/i18n/messages/sk.json | 197 +++++++++------ src/i18n/messages/sv.json | 199 ++++++++------- src/i18n/messages/sw.json | 199 ++++++++------- src/i18n/messages/ta.json | 199 ++++++++------- src/i18n/messages/te.json | 199 ++++++++------- src/i18n/messages/th.json | 199 ++++++++------- src/i18n/messages/tr.json | 199 ++++++++------- src/i18n/messages/uk-UA.json | 199 ++++++++------- src/i18n/messages/ur.json | 199 ++++++++------- src/i18n/messages/vi.json | 71 +++--- src/i18n/messages/zh-CN.json | 157 ++++++------ src/i18n/messages/zh-TW.json | 181 +++++++------- src/lib/db/migrationRunner.ts | 12 + .../160_rename_freepik_to_magnific.sql | 41 +++ src/lib/modelMetadataRegistry.ts | 1 - src/lib/providers/catalog.ts | 4 +- src/lib/providers/imageValidation.ts | 19 +- src/lib/providers/validation.ts | 3 + src/shared/constants/providers.ts | 27 +- .../providers/apikey/specialty-media.ts | 15 +- .../validation/compressionConfigSchemas.ts | 2 + stryker.conf.json | 25 +- tests/snapshots/provider/translate-path.json | 101 ++++++++ tests/unit/alibaba-image-media.test.ts | 2 +- .../antigravity-retired-public-models.test.ts | 1 + tests/unit/freepik-image-handler.test.ts | 163 ------------ ...ard-session-lease-bypass-inventory.test.ts | 16 +- ...instrumentation-warm-catalog-cache.test.ts | 13 +- tests/unit/magnific-image-handler.test.ts | 233 ++++++++++++++++++ tests/unit/provider-alias-uniqueness.test.ts | 8 + .../provider-validation-image-only.test.ts | 22 ++ tests/unit/redirects-cli-renames.test.ts | 8 + ...on-affinity-combo-timeout-eviction.test.ts | 9 +- tests/unit/sse-heartbeat.test.ts | 32 ++- 139 files changed, 5734 insertions(+), 4000 deletions(-) create mode 100644 changelog.d/fixes/10594-freepik-magnific-api.md delete mode 100644 open-sse/config/providers/registry/freepik/index.ts create mode 100644 open-sse/config/providers/registry/magnific/index.ts rename open-sse/handlers/imageGeneration/providers/{freepik.ts => magnific.ts} (74%) create mode 100644 src/lib/db/migrations/160_rename_freepik_to_magnific.sql delete mode 100644 tests/unit/freepik-image-handler.test.ts create mode 100644 tests/unit/magnific-image-handler.test.ts diff --git a/.env.example b/.env.example index 698c09329b..20c43643a2 100644 --- a/.env.example +++ b/.env.example @@ -714,6 +714,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # ALL_PROXY=socks5://127.0.0.1:7890 # NO_PROXY=localhost,127.0.0.1 +# Pin the echo-IP target used by proxy egress probes. Unset, the probe tries +# api64.ipify.org then api4.ipify.org so IPv4-only tunnels are not reported dead. +# Used by: src/lib/proxyEchoTarget.ts. +# OMNIROUTE_PROXY_ECHO_URL=https://api4.ipify.org?format=json + # Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher. # Long-lived SSE streams such as Codex /v1/responses need more than one # connection when multiple requests share the same account-level proxy. @@ -894,6 +899,14 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # Set to 0/false/off to skip compression entirely. Default: rtk # OMNIROUTE_MCP_DESCRIPTION_COMPRESSION=rtk +# Abort budget (ms) for MCP-server internal management reads (health, resilience, +# combos, quota, usage). Default: 10000. Used by: open-sse/mcp-server/fetchTimeout.ts +# OMNIROUTE_MCP_FETCH_TIMEOUT_MS=10000 + +# Abort budget (ms) for MCP hops that wait on a provider (route_request, web_search, +# web_fetch). Default: 60000. Used by: open-sse/mcp-server/fetchTimeout.ts +# OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS=60000 + # Model catalog sync interval in hours. # Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh. # Default: 24 @@ -2165,6 +2178,19 @@ APP_LOG_TO_FILE=true # Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: detect local install, else pin. # CURSOR_AGENT_CLI_VERSION=2026.07.08-0c04a8a +# Path to the Cursor Agent binary used for image generation. +# Used by: open-sse/handlers/imageGeneration/providers (CURSOR_IMAGE.md). +# CURSOR_AGENT_BIN=/path/to/agent + +# Cursor image-generation wall clock (ms). Default: 210000. +# CURSOR_IMG_TIMEOUT_MS=210000 + +# Shared-seat concurrency gate for Cursor image jobs. Default: 2. +# CURSOR_IMG_MAX_CONCURRENT=2 + +# Override Cursor CLI --model for image jobs. Default: request model / auto. +# CURSOR_IMG_MODEL=auto + # Cursor Agent CLI data directory override (versions live under /versions/). # Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: ~/.local/share/cursor-agent (unix) # or %LOCALAPPDATA%\cursor-agent (win32). Official agent CLI also honors this var. diff --git a/AGENTS.md b/AGENTS.md index 30e1f6d27c..d4a7e7eb80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below. | Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) | | Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | | Services | `open-sse/services/` | Combo routing, rate limits, caching, etc | -| Database | `src/lib/db/` | SQLite domain modules (154 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (157 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/README.md b/README.md index 2293040ff3..0d5ea9f25f 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
-The Promise — One endpoint. 346 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 346 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests). +The Promise — One endpoint. 346 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 346 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 57 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests).

@@ -646,7 +646,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) -> The most complete catalog of any open-source router: **346 providers**, **90+ with a free tier**, **56 free forever**. +> The most complete catalog of any open-source router: **346 providers**, **90+ with a free tier**, **57 free forever**.
@@ -1174,7 +1174,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 154 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 157 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) diff --git a/changelog.d/fixes/10594-freepik-magnific-api.md b/changelog.d/fixes/10594-freepik-magnific-api.md new file mode 100644 index 0000000000..4c1c59a701 --- /dev/null +++ b/changelog.d/fixes/10594-freepik-magnific-api.md @@ -0,0 +1 @@ +- **fix(providers):** Magnific Mystic is now the canonical provider (`/dashboard/providers/magnific`, `magnific/`). It uses the Magnific API (`api.magnific.com` + `x-magnific-api-key`), dashboard Test Connection validates keys without starting a paid generation, and the old `freepik` slug remains a legacy alias ([#10594](https://github.com/diegosouzapw/OmniRoute/pull/10594)) diff --git a/config/quality/open-sse-typecheck-baseline.json b/config/quality/open-sse-typecheck-baseline.json index 9b900ce2bd..c6de98b418 100644 --- a/config/quality/open-sse-typecheck-baseline.json +++ b/config/quality/open-sse-typecheck-baseline.json @@ -2,28 +2,10 @@ "open-sse/handlers/chatCore/clientUsageBuffer.ts": { "TS2345": 2 }, - "open-sse/services/browserBackedChat.ts": { - "TS2353": 2 - }, - "open-sse/services/compression/engines/omniglyphAdapter.ts": { - "TS2307": 1 - }, - "open-sse/services/compression/stats.ts": { - "TS2307": 1 - }, - "open-sse/utils/cursorImages.ts": { - "TS2339": 1 - }, - "open-sse/utils/imageNormalize.ts": { - "TS2339": 1 - }, "open-sse/utils/stream.ts": { "TS2345": 2, "TS2322": 2 }, - "open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/markdown.ts": { - "TS2307": 2 - }, "src/lib/guardrails/videoBridgeHelpers.ts": { "TS2488": 1, "TS2365": 2, diff --git a/docs/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg index 507e8ac54c..4fd6887859 100644 --- a/docs/diagrams/cli-terminal.svg +++ b/docs/diagrams/cli-terminal.svg @@ -1,6 +1,6 @@ - + Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen. - + @@ -16,12 +16,12 @@ OmniRoute Providers1f3a9c2e  anthropic   Claude Max 20x    active8c2d5b1a  codex       Codex Pro (team)  activef4e0a97b  glm         GLM Coding Plan   active03bd6e5f  kimi        Kimi K2 free      active… 334 more providers - + $ omniroute combo list - - + + OmniRoute Combos   always-on     [priority      ] enabled   cost-saver    [cost-optimized] enabled   fusion-panel  [fusion        ] enabled   context-relay [context-relay ] enabled… run: omniroute combo create diff --git a/docs/diagrams/comparison-table.svg b/docs/diagrams/comparison-table.svg index e69c2d0c2c..d73a1fb255 100644 --- a/docs/diagrams/comparison-table.svg +++ b/docs/diagrams/comparison-table.svg @@ -1,4 +1,4 @@ - + Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses. diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg index c73ef5e0d7..1c3f0a6cc9 100644 --- a/docs/diagrams/promise-pillars.svg +++ b/docs/diagrams/promise-pillars.svg @@ -1,4 +1,4 @@ - + Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle. @@ -21,7 +21,7 @@ - One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. + One endpoint. 346 providers. Never stop building — OmniRoute picks the cheapest one that works. @@ -38,7 +38,7 @@ Never hit limits - Auto-fallback across 343 providers in + Auto-fallback across 346 providers in milliseconds. Quota out? The next provider takes over — zero downtime. diff --git a/docs/diagrams/readme-hero.svg b/docs/diagrams/readme-hero.svg index e5ad1f9971..fb332a4574 100644 --- a/docs/diagrams/readme-hero.svg +++ b/docs/diagrams/readme-hero.svg @@ -1,4 +1,4 @@ - + Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame. @@ -28,7 +28,7 @@ Never stop coding. - Every AI tool → 343 providers90+ free — through one endpoint. + Every AI tool → 346 providers90+ free — through one endpoint. Claude Code · Codex · Cursor · Cline · Copilot · Antigravity  →  FREE Claude / GPT / Gemini · auto-fallback diff --git a/docs/guides/MANAGEMENT-AUTH.md b/docs/guides/MANAGEMENT-AUTH.md index 31391e4b7f..5e0d8d6eeb 100644 --- a/docs/guides/MANAGEMENT-AUTH.md +++ b/docs/guides/MANAGEMENT-AUTH.md @@ -14,8 +14,8 @@ Canonical implementation: `src/lib/api/requireManagementAuth.ts`. | Credential | Typical form | Created where | Intended use | Management capability | |---|---|---|---|---| -| Dashboard session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules | -| Local CLI machine token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only | +| Dashboard JWT session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules | +| CLI machine-id token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only | | Scoped Access Token | `oma_live_…` | **Settings → Access Tokens** or `omniroute connect` | Remote CLI and management API | Must satisfy the route's required `read`, `write`, or `admin` scope | | Inference API key | `sk-…` (and other API-key prefixes) | **API Manager / API Keys** | `/v1/*` inference | **None** unless the key metadata includes `manage` or `admin` | @@ -61,13 +61,13 @@ chat client key for automation unless you deliberately granted that scope. ## How to create and revoke -### Dashboard session +### Dashboard JWT session 1. Open `/login`, sign in with the management password (`INITIAL_PASSWORD` on first boot). 2. Cookie `auth_token` is HttpOnly. Browser dashboard uses it automatically. 3. Log out via `/api/auth/logout`. There is no long-lived secret to copy. -### Local CLI machine token +### CLI machine-id token 1. Run `omniroute` on the **same host** as the server (loopback). 2. The CLI bootstraps a machine-id token under `~/.omniroute/` (chmod 600). diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 732d79189c..53dfd1c0d6 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index e930a1f05a..cf0018a415 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index e930a1f05a..cf0018a415 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index baa6656839..2749b7b108 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index dfb5f9b2b8..76a283564b 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index 10ce4811ac..5bcb53ef10 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index b5ebeb9c86..17cb4a70bf 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index 72d4fa4a05..0441be519d 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 4b324a992d..c81ec3a853 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 92d30e036c..974084ef14 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index 2b5cdefa71..6db495b3d9 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index d0f0f42f59..885f78df63 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index f8f0b3f644..617ce45e8c 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index d79717b61e..67b0cfeba3 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 1b941f1e5a..fa74ab5997 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index e1b23c5e3d..71572bd5a4 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 7713dacb68..c6f22ffc46 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index e0823ba499..db4f6a9cfd 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 5c1963195f..f021f83d5a 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 2ae8fe4ec4..52cf209a74 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 157eec643a..cc3125f845 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index e26b195208..650ac31922 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index 67f935d590..bdd29b2d01 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index e28dda1508..7cb57aa53c 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index ed539c84be..73ea9e49ba 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index f8665fa410..7d1d8be0f2 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index c56d966794..59be9758e4 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index 6e2aa2bb20..1f4f205d44 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index 4fac614baf..485f971d1f 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 9d522135b2..319111231a 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index df0dbd95a7..edb3477ef2 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 59246e38bf..48847120cb 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index dbbf236c5d..f90e0ec031 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index 842ca8c7ee..8f64c7aab8 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 5098dbc0ae..483d2f8a41 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index f1251db7be..ac3a14103a 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index ba729555c2..d6e2fbd674 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 33b12b870b..17e04f3ce1 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index bd5d5fdc83..9291f52642 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index 4b59fd5d3d..0cc3e4e22e 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 122734bec7..28d5e6fe23 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index b1f841f8e9..ae8fd155b5 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -4,7 +4,7 @@ --- -> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. +> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 346 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app. ## Overview @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -169,7 +169,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ └── manager.ts # MITM proxy manager │ ├── shared/ # Shared utilities, components, and constants │ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.) -│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes +│ │ ├── constants/ # Provider definitions (346), model lists, pricing, routing strategies, MCP scopes │ │ ├── contracts/ # Shared API contracts │ │ ├── hooks/ # React hooks │ │ ├── middleware/ # Shared middleware utilities @@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo ## Key Features (v3.8.50) ### Core Proxy -- **342 AI providers** with automatic format translation +- **346 AI providers** with automatic format translation - **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible) - **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline - **4-tier fallback**: Subscription → API Key → Cheap → Free @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. @@ -479,7 +479,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool ## v3.8.x Highlights -- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add +- **346-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add - **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay` - **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown - **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index c475f2cadf..401d7f6a30 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -505,6 +505,8 @@ detection above). | `OMNIROUTE_MCP_SCOPES` | _(all)_ | `open-sse/mcp-server/server.ts` | Comma-separated scopes: `admin`, `combos`, `health`, `models`, `routing`, `budget`, `metrics`, `pricing`, `memory`, `skills`. | | `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | `false` | `open-sse/mcp-server/descriptionCompressor.ts` | Compress MCP tool descriptions before serializing the manifest. Enable values: `1`, `true`, `on`. | | `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | `rtk` | `open-sse/mcp-server/descriptionCompressor.ts` | Compression algorithm/profile. Disable values: `0`, `false`, `off`. | +| `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` | `10000` | `open-sse/mcp-server/fetchTimeout.ts` | Abort budget (ms) for MCP-server internal management reads (health, resilience, combos, quota, usage). | +| `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS` | `60000` | `open-sse/mcp-server/fetchTimeout.ts` | Abort budget (ms) for MCP hops that wait on a provider (`route_request`, `web_search`, `web_fetch`). | | `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. | | `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/lib/usage/providerLimits.ts` | Provider rate-limit and quota polling interval. | | `PROVIDER_LIMITS_SYNC_SPACING_MS` | `1500` | `src/lib/usage/providerLimits.ts` | Gap (ms) between consecutive OAuth quota fetches in a bulk sync; OAuth connections are fetched one at a time to avoid bursting an upstream. `0` opts out (concurrent). | @@ -1117,6 +1119,10 @@ changing them requires a code edit, not an env var: | `CURSOR_IMAGE_FETCH_TIMEOUT_MS` | `15000` | `open-sse/utils/cursorImages.ts` | Per-image fetch timeout (ms) for remote `image_url` vision input. | | `CURSOR_STATE_DB_PATH` | _(probed)_ | `open-sse/utils/cursorVersionDetector.ts` | Override the Cursor IDE state DB lookup used for IDE version detection. | | `CURSOR_AGENT_CLI_VERSION` | _(detect / pin)_ | `open-sse/utils/cursorAgentCliVersion.ts` | Agent CLI build id (`YYYY.MM.DD-`) for `x-cursor-client-version: cli-…` on Agent Run. | +| `CURSOR_AGENT_BIN` | _(unset)_ | `open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts` | Path to the Cursor Agent binary used for image generation. Unset, the handler uses `providerSpecificData.agentBin` then PATH. | +| `CURSOR_IMG_TIMEOUT_MS` | `210000` | `open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts` | Per-image wall clock (ms) for Cursor Agent image jobs. | +| `CURSOR_IMG_MAX_CONCURRENT` | `2` | `open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts` | Shared-seat concurrency gate for Cursor image jobs. | +| `CURSOR_IMG_MODEL` | request / `auto` | `open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts` | Override Cursor CLI `--model` for image jobs. | | `CURSOR_DATA_DIR` | _(probed)_ | `open-sse/utils/cursorAgentCliVersion.ts` | Override Cursor Agent CLI data dir (`…/versions/`); same var the official agent uses. | | `CURSOR_TOKEN` | _(unset)_ | `scripts/ad-hoc/cursor-tap.cjs` | Direct Cursor bearer token used by developer tooling. | | `OMNIROUTE_LOG_REQUEST_SHAPE` | disabled (opt-in via `"1"`) | `src/app/api/v1/chat/completions/route.ts` | Log content-type/length markers for large chat payloads when `"1"` is set. Off by default to reduce log noise. | diff --git a/llm.txt b/llm.txt index 15a03831d1..da0da5334e 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 154 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 157 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 154 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 157 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/next.config.mjs b/next.config.mjs index 2f22b8aebd..df3f6e32c4 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -438,6 +438,11 @@ const nextConfig = { destination: "/dashboard/omni-skills", permanent: true, }, + { + source: "/dashboard/providers/freepik", + destination: "/dashboard/providers/magnific", + permanent: true, + }, // Architecture { source: "/docs/architecture", diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 650dcdd69c..02019dc4a0 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -8,7 +8,7 @@ import { LMARENA_DIRECT_IMAGE_MODELS } from "./providers/registry/lmarena/directModels.ts"; import { SEGMIND_IMAGE_PROVIDER } from "./providers/registry/segmind/imageModels.ts"; import { KIE_IMAGE_MODELS } from "./providers/registry/kie/imageModels.ts"; -import { FREEPIK_IMAGE_PROVIDER } from "./providers/registry/freepik/index.ts"; +import { MAGNIFIC_IMAGE_PROVIDER } from "./providers/registry/magnific/index.ts"; import { STABILITY_AI_IMAGE_MODELS } from "./providers/registry/stability-ai/imageModels.ts"; import { CHEAPERINFERENCE_IMAGE_PROVIDER } from "./providers/registry/cheaperinference/imageModels.ts"; import { @@ -495,7 +495,7 @@ export const IMAGE_PROVIDERS: Record = { ], supportedSizes: ["1024x1024", "1024x1792", "1792x1024"], }, - freepik: FREEPIK_IMAGE_PROVIDER, + magnific: MAGNIFIC_IMAGE_PROVIDER, sdwebui: { id: "sdwebui", baseUrl: "http://localhost:7860/sdapi/v1/txt2img", @@ -884,7 +884,12 @@ export const IMAGE_PROVIDERS: Record = { * Get image provider config by ID */ export function getImageProvider(providerId) { - return IMAGE_PROVIDERS[providerId] || null; + if (IMAGE_PROVIDERS[providerId]) return IMAGE_PROVIDERS[providerId]; + if (!providerId) return null; + for (const config of Object.values(IMAGE_PROVIDERS)) { + if (config.alias === providerId) return config; + } + return null; } /** @@ -1021,12 +1026,7 @@ export function getImageModelEntry(modelStr) { }; } -/** - * An image input is only MANDATORY for edit-only models — those whose modalities - * are `["image"]` with no `"text"`. Models listing both `["text", "image"]` accept - * an image but can also run pure text-to-image, so they must NOT be gated on an - * image input (that gate previously blocked 41 dual-modality t2i models). - */ +/** Image input is mandatory only for edit-only models (`["image"]`, no `"text"`). Dual-modality models also accept pure t2i. */ export function modalitiesRequireImageInput(inputModalities) { const list = Array.isArray(inputModalities) ? inputModalities : ["text"]; return list.includes("image") && !list.includes("text"); diff --git a/open-sse/config/providers/registry/freepik/index.ts b/open-sse/config/providers/registry/freepik/index.ts deleted file mode 100644 index 7b99c71168..0000000000 --- a/open-sse/config/providers/registry/freepik/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Freepik (Magnific Mystic) image provider registry entry. - * Extracted into its own module to keep open-sse/config/imageRegistry.ts - * under the file-size cap (god-file decomposition; semantic split). - */ -export const FREEPIK_IMAGE_PROVIDER = { - id: "freepik", - // Freepik rebranded its API docs to Magnific in April 2026; the Mystic - // endpoint itself still lives under api.freepik.com as of this writing - // (docs.freepik.com redirects to docs.magnific.com, but the API host - // has not moved). Re-verify against live docs if this ever 404s. - baseUrl: "https://api.freepik.com/v1/ai/mystic", - statusUrl: "https://api.freepik.com/v1/ai/mystic", - authType: "apikey", - authHeader: "x-freepik-api-key", - format: "freepik-image", // custom: async submit task_id, then poll GET /{task_id} - models: [ - { id: "realism", name: "Mystic Realism" }, - { id: "fluid", name: "Mystic Fluid (Imagen 3)" }, - { id: "zen", name: "Mystic Zen" }, - { id: "flexible", name: "Mystic Flexible" }, - { id: "super_real", name: "Mystic Super Real" }, - { id: "editorial_portraits", name: "Mystic Editorial Portraits" }, - ], - supportedSizes: ["1024x1024", "1024x1792", "1792x1024"], -}; diff --git a/open-sse/config/providers/registry/magnific/index.ts b/open-sse/config/providers/registry/magnific/index.ts new file mode 100644 index 0000000000..63c83d5b1c --- /dev/null +++ b/open-sse/config/providers/registry/magnific/index.ts @@ -0,0 +1,26 @@ +/** + * Magnific Mystic image provider registry entry. + * Extracted into its own module to keep open-sse/config/imageRegistry.ts + * under the file-size cap (god-file decomposition; semantic split). + */ +export const MAGNIFIC_IMAGE_PROVIDER = { + id: "magnific", + // Official Magnific API (docs.magnific.com). The previous OmniRoute slug + // was `freepik` because Magnific started as Freepik's developer API; keep + // that id as a legacy alias so old URLs and `freepik/` still resolve. + alias: "freepik", + baseUrl: "https://api.magnific.com/v1/ai/mystic", + statusUrl: "https://api.magnific.com/v1/ai/mystic", + authType: "apikey", + authHeader: "x-magnific-api-key", + format: "magnific-image", // custom: async submit task_id, then poll GET /{task_id} + models: [ + { id: "realism", name: "Mystic Realism" }, + { id: "fluid", name: "Mystic Fluid (Imagen 3)" }, + { id: "zen", name: "Mystic Zen" }, + { id: "flexible", name: "Mystic Flexible" }, + { id: "super_real", name: "Mystic Super Real" }, + { id: "editorial_portraits", name: "Mystic Editorial Portraits" }, + ], + supportedSizes: ["1024x1024", "1024x1792", "1792x1024"], +}; diff --git a/open-sse/executors/copilot-m365-connection.ts b/open-sse/executors/copilot-m365-connection.ts index d5f6d80c6c..c8251af5f4 100644 --- a/open-sse/executors/copilot-m365-connection.ts +++ b/open-sse/executors/copilot-m365-connection.ts @@ -8,6 +8,7 @@ * the URL MUST go through redactWsUrl(). */ +import { resolvePublicCred } from "../utils/publicCreds.ts"; import { randomUUID, randomBytes } from "node:crypto"; import type { ProviderCredentials } from "./base.ts"; @@ -272,7 +273,7 @@ export function redactWsUrl(wsUrl: string): string { // of requiring a fresh DevTools capture after every expiry. /** Public client id observed in both the browser token and M365-Copilot2API. */ -export const M365_OAUTH_CLIENT_ID = "c0ab8ce9-e9a0-42e7-b064-33d422df41f1"; +export const M365_OAUTH_CLIENT_ID = resolvePublicCred("m365_oauth_client_id"); export const M365_OAUTH_SCOPE = "openid profile offline_access https://substrate.office.com/sydney/M365Chat.Read " + diff --git a/open-sse/executors/copilot-m365-web.ts b/open-sse/executors/copilot-m365-web.ts index 57f854ef86..5bdaf9ae9b 100644 --- a/open-sse/executors/copilot-m365-web.ts +++ b/open-sse/executors/copilot-m365-web.ts @@ -320,15 +320,16 @@ export class CopilotM365WebExecutor extends BaseExecutor { const rotated = result.refreshToken || refreshToken; const chathubPath = currentM365ChathubPath(credentials); + const assembledApiKey = chathubPath + ? ["access_token=", result.accessToken, "; chathubPath=", chathubPath].join("") + : ""; const next = { ...credentials, accessToken: result.accessToken, refreshToken: rotated, // Keep the pasted-format apiKey self-consistent so every resolution path // (fresh column, stale column, dashboard re-read) sees the same token. - ...(chathubPath - ? { apiKey: `access_token=${result.accessToken}; chathubPath=${chathubPath}` } - : {}), + ...(assembledApiKey ? { apiKey: assembledApiKey } : {}), ...(result.expiresIn ? { expiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString() } : {}), diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index d82452bb7e..8678fb6105 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -1,5 +1,6 @@ import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts"; import { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor } from "./registry.ts"; +import type { BaseExecutor } from "./base.ts"; import { AntigravityExecutor } from "./antigravity.ts"; import { GithubExecutor } from "./github.ts"; import { GheCopilotExecutor } from "./ghe-copilot.ts"; @@ -233,7 +234,7 @@ const executors = { // Bootstrap: register every built-in in the ExecutorRegistry. registerExecutor // throws on duplicates, so an alias collision fails at module load, exactly as // loudly as a duplicate object key would have failed at lint time. -for (const [alias, executor] of Object.entries(executors)) { +for (const [alias, executor] of Object.entries(executors) as [string, BaseExecutor][]) { registerExecutor(alias, executor); } diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index a2053bb556..c8312ab974 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -44,7 +44,7 @@ import { handleImagen3ImageGeneration } from "./imageGeneration/providers/imagen import { handleIdeogramImageGeneration } from "./imageGeneration/providers/ideogram.ts"; import { handleHaiperImageGeneration } from "./imageGeneration/providers/haiper.ts"; import { handleLeonardoImageGeneration } from "./imageGeneration/providers/leonardo.ts"; -import { handleFreepikImageGeneration } from "./imageGeneration/providers/freepik.ts"; +import { handleMagnificImageGeneration } from "./imageGeneration/providers/magnific.ts"; import { handleChatGptWebImageGeneration, extractMarkdownImageUrls, @@ -631,8 +631,8 @@ export async function handleImageGeneration({ log, }); } - if (providerConfig.format === "freepik-image") { - return handleFreepikImageGeneration({ + if (providerConfig.format === "magnific-image" || providerConfig.format === "freepik-image") { + return handleMagnificImageGeneration({ model, provider, providerConfig, diff --git a/open-sse/handlers/imageGeneration/providers/freepik.ts b/open-sse/handlers/imageGeneration/providers/magnific.ts similarity index 74% rename from open-sse/handlers/imageGeneration/providers/freepik.ts rename to open-sse/handlers/imageGeneration/providers/magnific.ts index 2f3320cf06..31ea686327 100644 --- a/open-sse/handlers/imageGeneration/providers/freepik.ts +++ b/open-sse/handlers/imageGeneration/providers/magnific.ts @@ -1,12 +1,11 @@ -// Freepik (Magnific Mystic) image generation adapter. +// Magnific Mystic image generation adapter. // Async submit->poll flow modeled on leonardo.ts's generationId pattern: // POST /v1/ai/mystic returns { data: { task_id, status } }, then // GET /v1/ai/mystic/{task_id} is polled until status is COMPLETED/FAILED. -// Docs: https://docs.magnific.com/api-reference/mystic (Freepik rebranded to -// Magnific in April 2026; both `api.freepik.com` and the newer -// `api.magnific.com` domain/header pair are in circulation during the -// transition, so the base URL and auth header both come from providerConfig -// rather than being hardcoded here). +// Docs: https://docs.magnific.com/api-reference/mystic +// Official host/header: api.magnific.com + x-magnific-api-key. +// Both come from providerConfig so a local override can still use the +// legacy api.freepik.com / x-freepik-api-key pair if needed. import { saveCallLog } from "@/lib/usageDb"; import { sleep } from "../../../utils/sleep.ts"; @@ -21,34 +20,34 @@ function normalizePositiveNumber(value: unknown, fallback: number): number { return Math.floor(n); } -interface FreepikProviderConfig { +interface MagnificProviderConfig { baseUrl: string; statusUrl?: string; authHeader?: string; } -interface FreepikCredentials { +interface MagnificCredentials { apiKey?: string; } -interface FreepikGenerationParams { +interface MagnificGenerationParams { model: string; provider: string; - providerConfig: FreepikProviderConfig; + providerConfig: MagnificProviderConfig; body: Record; - credentials: FreepikCredentials; + credentials: MagnificCredentials; log?: { info: (tag: string, msg: string) => void; error: (tag: string, msg: string) => void }; } -interface FreepikImageResult { +interface MagnificImageResult { success: boolean; status?: number; error?: string; data?: { created: number; data: Array<{ b64_json: string }> }; } -function freepikAuthHeader(providerConfig: FreepikProviderConfig, token: string) { - const headerName = providerConfig.authHeader || "x-freepik-api-key"; +function magnificAuthHeader(providerConfig: MagnificProviderConfig, token: string) { + const headerName = providerConfig.authHeader || "x-magnific-api-key"; return { [headerName]: token }; } @@ -58,7 +57,7 @@ async function logAndFail(params: { startTime: number; status: number; error: string; -}): Promise { +}): Promise { const { provider, model, startTime, status, error } = params; const sanitized = sanitizeErrorMessage(error); saveCallLog({ @@ -74,7 +73,7 @@ async function logAndFail(params: { } async function submitMysticTask(params: { - providerConfig: FreepikProviderConfig; + providerConfig: MagnificProviderConfig; token: string; model: string; prompt: string; @@ -85,7 +84,7 @@ async function submitMysticTask(params: { method: "POST", headers: { "Content-Type": "application/json", - ...freepikAuthHeader(providerConfig, token), + ...magnificAuthHeader(providerConfig, token), }, body: JSON.stringify({ prompt, @@ -97,14 +96,14 @@ async function submitMysticTask(params: { } async function pollMysticTask(params: { - providerConfig: FreepikProviderConfig; + providerConfig: MagnificProviderConfig; token: string; taskId: string; }): Promise<{ status: string; imageUrl?: string }> { const { providerConfig, token, taskId } = params; const statusBase = providerConfig.statusUrl || providerConfig.baseUrl; const res = await fetch(`${statusBase}/${taskId}`, { - headers: { ...freepikAuthHeader(providerConfig, token) }, + headers: { ...magnificAuthHeader(providerConfig, token) }, }); const json = await res.json(); const task = json?.data || json; @@ -113,9 +112,9 @@ async function pollMysticTask(params: { return { status, imageUrl: typeof generated[0] === "string" ? generated[0] : undefined }; } -async function downloadGeneratedImage(imageUrl: string): Promise< - { state: "ok"; b64: string } | { state: "failed"; status: number; error: string } -> { +async function downloadGeneratedImage( + imageUrl: string +): Promise<{ state: "ok"; b64: string } | { state: "failed"; status: number; error: string }> { const imgRes = await fetch(imageUrl); if (!imgRes.ok) { return { @@ -133,7 +132,7 @@ async function resolveCompletedResult(params: { model: string; startTime: number; imageUrl?: string; -}): Promise { +}): Promise { const { provider, model, startTime, imageUrl } = params; if (!imageUrl) { return logAndFail({ @@ -141,7 +140,7 @@ async function resolveCompletedResult(params: { model, startTime, status: 502, - error: "Freepik Mystic completed without a generated image URL", + error: "Magnific Mystic completed without a generated image URL", }); } const downloaded = await downloadGeneratedImage(imageUrl); @@ -163,7 +162,7 @@ async function resolveCompletedResult(params: { } async function pollUntilDone(params: { - providerConfig: FreepikProviderConfig; + providerConfig: MagnificProviderConfig; token: string; taskId: string; provider: string; @@ -171,9 +170,17 @@ async function pollUntilDone(params: { startTime: number; pollIntervalMs: number; pollTimeoutMs: number; -}): Promise { - const { providerConfig, token, taskId, provider, model, startTime, pollIntervalMs, pollTimeoutMs } = - params; +}): Promise { + const { + providerConfig, + token, + taskId, + provider, + model, + startTime, + pollIntervalMs, + pollTimeoutMs, + } = params; const deadline = Date.now() + pollTimeoutMs; while (Date.now() < deadline) { @@ -189,7 +196,7 @@ async function pollUntilDone(params: { model, startTime, status: 502, - error: "Freepik Mystic image generation failed", + error: "Magnific Mystic image generation failed", }); } } @@ -199,24 +206,32 @@ async function pollUntilDone(params: { model, startTime, status: 504, - error: "Freepik Mystic image generation timed out", + error: "Magnific Mystic image generation timed out", }); } async function submitAndGetTaskId(params: { - providerConfig: FreepikProviderConfig; + providerConfig: MagnificProviderConfig; token: string; model: string; prompt: string; body: Record; provider: string; startTime: number; -}): Promise<{ taskId: string } | { failed: FreepikImageResult }> { +}): Promise<{ taskId: string } | { failed: MagnificImageResult }> { const { providerConfig, token, model, prompt, body, provider, startTime } = params; const res = await submitMysticTask({ providerConfig, token, model, prompt, body }); if (!res.ok) { const errorText = await res.text(); - return { failed: await logAndFail({ provider, model, startTime, status: res.status, error: errorText }) }; + return { + failed: await logAndFail({ + provider, + model, + startTime, + status: res.status, + error: errorText, + }), + }; } const submitJson = await res.json(); @@ -228,28 +243,31 @@ async function submitAndGetTaskId(params: { model, startTime, status: 502, - error: "Freepik Mystic did not return a task_id", + error: "Magnific Mystic did not return a task_id", }), }; } return { taskId }; } -export async function handleFreepikImageGeneration({ +export async function handleMagnificImageGeneration({ model, provider, providerConfig, body, credentials, log, -}: FreepikGenerationParams): Promise { +}: MagnificGenerationParams): Promise { const startTime = Date.now(); const token = credentials?.apiKey || ""; const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? ""); const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, DEFAULT_POLL_INTERVAL_MS); const pollTimeoutMs = normalizePositiveNumber(body.poll_timeout_ms, DEFAULT_POLL_TIMEOUT_MS); if (log) { - log.info("IMAGE", `${provider}/${model} (freepik-mystic) | prompt: "${prompt.slice(0, 60)}..."`); + log.info( + "IMAGE", + `${provider}/${model} (magnific-mystic) | prompt: "${prompt.slice(0, 60)}..."` + ); } try { @@ -276,7 +294,7 @@ export async function handleFreepikImageGeneration({ }); } catch (err) { const message = (err as Error)?.message || String(err); - if (log) log.error("IMAGE", `${provider} freepik error: ${sanitizeErrorMessage(message)}`); + if (log) log.error("IMAGE", `${provider} magnific error: ${sanitizeErrorMessage(message)}`); return logAndFail({ provider, model, diff --git a/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts b/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts index 52ce967e1f..8b7f3cee32 100644 --- a/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts +++ b/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts @@ -87,6 +87,9 @@ describe("GLM Coding provider registry surfaces", () => { expect(PROVIDER_ID_TO_ALIAS.glm).toBe("glm"); expect(byProviderId).toEqual(byAlias); expect(byProviderId.map((model) => model.id)).toEqual([ + "glm-5.3", + "glm-5.3-high", + "glm-5.3-low", "glm-5.2", "glm-5.2-high", "glm-5.2-max", diff --git a/open-sse/mcp-server/fetchTimeout.ts b/open-sse/mcp-server/fetchTimeout.ts index a9389b0c24..e3cea5bcce 100644 --- a/open-sse/mcp-server/fetchTimeout.ts +++ b/open-sse/mcp-server/fetchTimeout.ts @@ -34,6 +34,21 @@ function readPositiveIntEnv(raw: string | undefined): number | null { return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null; } +function readMcpTimeoutOverride( + kind: McpFetchTimeoutKind, + env: Record +): string | undefined { + // Direct process.env member access so fabricated-docs / env-doc-sync see + // the operator knobs. Tests inject a fake env object and keep using the + // exported constant keys. + if (env === process.env) { + return kind === "upstream" + ? process.env.OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS + : process.env.OMNIROUTE_MCP_FETCH_TIMEOUT_MS; + } + return env[kind === "upstream" ? MCP_UPSTREAM_FETCH_TIMEOUT_ENV : MCP_FETCH_TIMEOUT_ENV]; +} + /** * Resolve the timeout for one internal fetch class. An unset, malformed or * non-positive override falls back to the built-in default rather than @@ -44,11 +59,8 @@ export function resolveMcpFetchTimeoutMs( kind: McpFetchTimeoutKind, env: Record = process.env ): number { - const upstream = kind === "upstream"; - const override = readPositiveIntEnv( - env[upstream ? MCP_UPSTREAM_FETCH_TIMEOUT_ENV : MCP_FETCH_TIMEOUT_ENV] - ); - return override ?? (upstream ? MCP_UPSTREAM_FETCH_TIMEOUT_MS : MCP_FETCH_TIMEOUT_MS); + const override = readPositiveIntEnv(readMcpTimeoutOverride(kind, env)); + return override ?? (kind === "upstream" ? MCP_UPSTREAM_FETCH_TIMEOUT_MS : MCP_FETCH_TIMEOUT_MS); } /** `AbortSignal` for one internal fetch of the given class. */ diff --git a/open-sse/translator/helpers/toolCallShim.ts b/open-sse/translator/helpers/toolCallShim.ts index 0c546bb299..ef00713474 100644 --- a/open-sse/translator/helpers/toolCallShim.ts +++ b/open-sse/translator/helpers/toolCallShim.ts @@ -89,8 +89,18 @@ const TOOL_SHIMS: Record = { }, }; +function resolveToolCallShim(name: string | undefined | null): ShimFn | undefined { + if (typeof name !== "string" || !name) return undefined; + if (Object.prototype.hasOwnProperty.call(TOOL_SHIMS, name)) return TOOL_SHIMS[name]; + const lower = name.toLowerCase(); + for (const [key, fn] of Object.entries(TOOL_SHIMS)) { + if (key.toLowerCase() === lower) return fn; + } + return undefined; +} + export function hasToolCallShim(name: string | undefined | null): boolean { - return typeof name === "string" && Object.prototype.hasOwnProperty.call(TOOL_SHIMS, name); + return Boolean(resolveToolCallShim(name)); } /** @@ -100,7 +110,7 @@ export function hasToolCallShim(name: string | undefined | null): boolean { * the shim with `{}` as input (so required arrays still get injected). */ export function applyToolCallShimToBuffer(name: string, raw: string): string { - const shim = TOOL_SHIMS[name]; + const shim = resolveToolCallShim(name); if (!shim) return raw; let parsed: unknown; diff --git a/open-sse/translator/response/claude-to-openai.ts b/open-sse/translator/response/claude-to-openai.ts index 026a3e1f3e..4905bd8e60 100644 --- a/open-sse/translator/response/claude-to-openai.ts +++ b/open-sse/translator/response/claude-to-openai.ts @@ -44,6 +44,40 @@ export function claudeToOpenAIResponse(chunk, state) { state.messageId = chunk.message?.id || `msg_${Date.now()}`; state.model = chunk.message?.model; state.toolCallIndex = 0; + const startUsage = chunk.message?.usage; + if (startUsage && typeof startUsage === "object") { + const inputTokens = + typeof startUsage.input_tokens === "number" + ? startUsage.input_tokens + : typeof startUsage.prompt_tokens === "number" + ? startUsage.prompt_tokens + : 0; + const outputTokens = + typeof startUsage.output_tokens === "number" + ? startUsage.output_tokens + : typeof startUsage.completion_tokens === "number" + ? startUsage.completion_tokens + : 0; + const cacheRead = + typeof startUsage.cache_read_input_tokens === "number" + ? startUsage.cache_read_input_tokens + : 0; + const cacheCreation = + typeof startUsage.cache_creation_input_tokens === "number" + ? startUsage.cache_creation_input_tokens + : 0; + if (inputTokens > 0 || outputTokens > 0 || cacheRead > 0 || cacheCreation > 0) { + const billableInputTokens = inputTokens + cacheRead; + state.usage = { + prompt_tokens: billableInputTokens, + completion_tokens: outputTokens, + input_tokens: billableInputTokens, + output_tokens: outputTokens, + }; + if (cacheRead > 0) state.usage.cache_read_input_tokens = cacheRead; + if (cacheCreation > 0) state.usage.cache_creation_input_tokens = cacheCreation; + } + } results.push(createChunk(state, { role: "assistant" })); break; } diff --git a/open-sse/utils/publicCreds.ts b/open-sse/utils/publicCreds.ts index 6d1e005b1d..ad915e4ce2 100644 --- a/open-sse/utils/publicCreds.ts +++ b/open-sse/utils/publicCreds.ts @@ -180,6 +180,12 @@ const EMBEDDED_DEFAULTS = { 13, 88, 13, 91, 68, 89, 65, 21, 72, 26, 21, 76, 0, 65, 93, 2, 26, 23, 28, 87, 14, 87, 8, 95, 12, 17, 70, 6, 24, 66, 17, 1, 10, 95, 81, 28, ], + // Microsoft 365 Copilot web (m365.cloud.microsoft) — public SPA client id + // observed in browser tokens and M365-Copilot2API. Not a per-user secret. + m365_oauth_client_id: [ + 12, 93, 15, 11, 74, 12, 16, 77, 72, 72, 73, 20, 82, 65, 93, 81, 72, 65, 28, 13, 93, 88, 93, 95, + 92, 70, 16, 81, 31, 66, 17, 4, 88, 88, 5, 28, + ], // Microsoft Edge Read Aloud (EdgeTTS) — public "trusted client token" used to // derive the Sec-MS-GEC anti-abuse header. Hardcoded in every known Edge // browser build and every open-source edge-tts reimplementation (e.g. diff --git a/open-sse/utils/usageTracking.ts b/open-sse/utils/usageTracking.ts index c89527518e..24fe802fda 100644 --- a/open-sse/utils/usageTracking.ts +++ b/open-sse/utils/usageTracking.ts @@ -567,6 +567,14 @@ export function sanitizeUsagePayloadForRequest( return replaceUsage(payload.message, "usage", FORMATS.CLAUDE); } if (payload.type === "message_delta" && payload.usage) { + // message_delta is output-only by spec. #10705 0-input repair would + // overwrite a valid message_start input count with an estimate. + const delta = payload.usage; + const deltaInput = + tokenNumber(delta.input_tokens) + + tokenNumber(delta.cache_read_input_tokens) + + tokenNumber(delta.cache_creation_input_tokens); + if (deltaInput === 0) return false; return replaceUsage(payload, "usage", FORMATS.CLAUDE); } if (payload.response?.usage) { diff --git a/skills/omni-combos-routing/SKILL.md b/skills/omni-combos-routing/SKILL.md index 9745bf9085..549e2b75e5 100644 --- a/skills/omni-combos-routing/SKILL.md +++ b/skills/omni-combos-routing/SKILL.md @@ -34,6 +34,28 @@ curl -X POST https://localhost:20128/api/combos \ -d '{}' ``` +### GET /api/combos/{id} + +Get combo by ID + +```bash +curl https://localhost:20128/api/combos/{id} \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### PUT /api/combos/{id} + +Update combo + +Partial update: the body is merged onto the stored combo, so a field left out keeps its current value. An array that IS sent replaces the stored one outright. + +```bash +curl -X PUT https://localhost:20128/api/combos/{id} \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{}' +``` + ### PATCH /api/combos/{id} Update combo diff --git a/skills/omni-inference/SKILL.md b/skills/omni-inference/SKILL.md index 2951ce794b..0dd4c541ca 100644 --- a/skills/omni-inference/SKILL.md +++ b/skills/omni-inference/SKILL.md @@ -27,7 +27,7 @@ returns 429 `WAITING_FOR_CAPACITY` with `Retry-After`. ```bash curl -X POST https://localhost:20128/api/v1/session-leases \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` @@ -147,7 +147,7 @@ Same handler as `POST /api/v1/embeddings`. Provided so Jina-compatible clients t ```bash curl -X POST https://localhost:20128/api/v1/multimodal-embeddings \ - -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Authorization: Bearer $OMNIROUTE_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` diff --git a/src/app/api/providers/route.ts b/src/app/api/providers/route.ts index 77c102bb0a..f1825674c2 100644 --- a/src/app/api/providers/route.ts +++ b/src/app/api/providers/route.ts @@ -17,6 +17,7 @@ import { isClaudeCodeCompatibleProvider, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, + resolveProviderId, } from "@/shared/constants/providers"; import { getConsistentMachineId } from "@/shared/utils/machineId"; import { syncToCloud } from "@/lib/cloudSync"; @@ -114,7 +115,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { - provider, + provider: requestedProvider, apiKey, name, priority, @@ -123,6 +124,7 @@ export async function POST(request: Request) { testStatus, providerSpecificData: incomingPsd, } = validation.data; + const provider = resolveProviderId(requestedProvider); // Business validation const isValidProvider = diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index a864ca3608..c56a543d56 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -519,13 +519,11 @@ async function buildUnifiedModelsResponseCore( const targetModel = getComboTargetModelId(target); if (!targetModel) return null; - const canonical = getCanonicalModelMetadata( - { - provider: targetModel.providerId, - model: targetModel.modelId, - }, - capabilityResolutionSnapshot - ); + const canonical = getCanonicalModelMetadata({ + provider: targetModel.providerId, + model: targetModel.modelId, + snapshot: capabilityResolutionSnapshot, + }); if (!canonical) return null; const providerId = canonical.provider || targetModel.providerId; diff --git a/src/app/api/v1/responses/route.ts b/src/app/api/v1/responses/route.ts index 98e914d574..a7d9978898 100644 --- a/src/app/api/v1/responses/route.ts +++ b/src/app/api/v1/responses/route.ts @@ -1,3 +1,4 @@ +import { z } from "zod"; import { handleChat } from "@/sse/handlers/chat"; import { CORS_HEADERS } from "@/shared/utils/cors"; import { createInjectionGuard } from "@/middleware/promptInjectionGuard"; @@ -114,9 +115,11 @@ async function postHandler(request: any) { } catch { return finishAdmission(errorResponse(400, "Invalid JSON body")); } - if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) { + const parsed = z.object({}).passthrough().safeParse(parsedBody); + if (!parsed.success || Array.isArray(parsed.data)) { return finishAdmission(errorResponse(400, "Request body must be a JSON object")); } + parsedBody = parsed.data; const structuralAdmission = await admitChatStructure(parsedBody, admission.lease, { sessionId, diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 329df3d883..85bb06ddb8 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1165,6 +1165,8 @@ "consoleLogs": "سجلات وحدة التحكم", "logsTimeline": "Timeline", "logsTimelineSubtitle": "جدول زمني للطلبات المرئية", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "التوجيه العام", "mitmProxy": "بروكسي MITM", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "فتح", "close": "إغلاق" }, - "noResults": "لا توجد نتائج", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "لا توجد نتائج" }, "webhooks": { "title": "خطافات الويب", @@ -1739,8 +1739,8 @@ "quotaShare": "مشاركة الحصة", "discovery": "الاستكشاف", "freeProviderRankings": "تصنيفات المزودين المجانيين", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "الفئات المجانية", "gamification": "التلعيب", "leaderboard": "لوحة الصدارة", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "اختر كيفية توزيع الطلبات على نماذجك؛ تتوفر 13 استراتيجية", "wizardStep4Title": "المراجعة والحفظ", "wizardStep4Desc": "راجع التكوين وفعّل المجموعة", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "تشغيل", "emailVisibilityStateOff": "إيقاف", "reorderHandle": "اسحب لإعادة الترتيب", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "لقد تم إهمال هذا المزود", "riskNotice": { "title": "قبل المتابعة", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "مزوّد له محاذير استخدام — انقر لعرض التفاصيل", "oauth": "يستخدم هذا المزوّد جلسة المنتج الرسمية أو OAuth، وهي غير مصرّح بها للاستخدام مع الوكيل أو الموجّه. لا نوصي بالاستخدام المكثف للوكلاء المستقلين (مثل OpenCloud والتدفقات الطويلة متعددة الخطوات والدُفعات الكبيرة)، فقد يقيّد مزوّد المنبع الحساب أو يحظره. استخدمه على مسؤوليتك.", "webCookie": "يصادق هذا المزوّد عبر ملفات تعريف ارتباط جلسة الويب. قد تُبطل خدمة المنبع الجلسة في أي وقت، ما يتطلب تسجيل الدخول مجددًا. لا يُنصح به للعمليات الطويلة غير المراقبة. استخدمه على مسؤوليتك.", @@ -5107,9 +5111,9 @@ "cancel": "إلغاء" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "معطل", "enableProvider": "تمكين المزود", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "تخطي {count} نموذج موجود", "autoSync": "المزامنة التلقائية", "autoSyncShort": "المزامنة", + "autoFetchModels": "جلب النماذج من المصدر تلقائيًا", + "autoFetchModelsTooltip": "استرجاع وتخزين نماذج المصدر عند الحاجة", + "autoFetchModelsEnabled": "تم تمكين جلب النموذج العلوي تلقائيًا", + "autoFetchModelsDisabled": "تم تعطيل جلب النموذج العلوي تلقائيًا", + "autoFetchModelsToggleFailed": "فشل في تبديل جلب النموذج العلوي تلقائيًا", + "autoFetchModelsPartialFailure": "تم تحديث بعض الاتصالات، لكن نموذج المصدر التلقائي لم يتغير في كل مكان", + "overridesUpstreamModel": "يتجاوز المصدر", + "overridesUpstreamModelHint": "إعداداتك تتجاوز هذا النموذج العلوي", + "resetToUpstreamDefaults": "استعادة الإعدادات الافتراضية للمصدر", + "resetToUpstreamDefaultsSuccess": "تم استعادة إعدادات النموذج الافتراضية من المصدر", + "resetToUpstreamDefaultsFailed": "فشل في استعادة إعدادات النموذج الافتراضية من المصدر", "autoSyncTooltip": "تحديث قائمة النماذج كل 24 ساعة (يمكن ضبطه عبر MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "تم تمكين المزامنة التلقائية — سيتم تحديث النماذج بشكل دوري", "autoSyncDisabled": "تم تعطيل المزامنة التلقائية", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "إعادة كتابة استدعاءات أداة web_fetch الأصلية إلى /v1/web/fetch الخاصة بـ OmniRoute.", "interceptionLoadError": "فشل تحميل إعدادات الاعتراض: {error}", "interceptionSaveError": "فشل حفظ إعدادات الاعتراض: {error}", - "ccAliasSectionTitle": "Expose في كود كلود (claude/…)", - "ccAliasSectionHint": "قم بالإعلان عن نماذج هذا المزود تحت معرفات المرآة claude/<provider>/<model> حتى يتمكن نموذج اكتشاف بوابة Claude Code من إدراجها. معطلة بشكل افتراضي - تمكين هذا يضاعف إدخالات الكتالوج لجميع العملاء.", - "ccAliasProviderLevelLabel": "موفر افتراضي", - "ccAliasModelOverridesLabel": "تجاوزات لكل نموذج", - "ccAliasModelOverrideAriaLabel": "تجاوز لـ {modelId}", - "ccAliasStateInherit": "وراثة", - "ccAliasStateOn": "تشغيل", - "ccAliasStateOff": "إيقاف", - "ccAliasAddModelPlaceholder": "معرف النموذج (مثل gpt-4o)", - "ccAliasAddModelButton": "إضافة تجاوز", - "ccAliasLoadError": "فشل في تحميل إعدادات discovery-alias: {error}", - "ccAliasSaveError": "فشل في حفظ إعداد alias الاكتشاف: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "ترويسات المنبع الإضافية", "compatUpstreamHeadersHint": "إعداد عالي الصلاحيات — يعامل معاملة بيانات اعتماد API الخاصة بالمزود، لذا لا ينبغي استخدامه إلا من مسؤولين موثوقين. تُدمج الترويسات بعد أن يضيف OmniRoute المصادقة. إذا استخدمت ترويسة مخصصة الاسم نفسه لترويسة موجودة (مثل Authorization)، فستستبدل قيمتك الترويسة المنشأة تلقائيًا بالكامل، بما فيها رمز Bearer. قد يؤدي الإعداد الخاطئ إلى خطأ 401 أو تعطّل مصادقة المنبع. أضف ترويسة واحدة في كل صف. تُحفظ القيمة عند فقدان التركيز أو إغلاق اللوحة.", "compatUpstreamHeaderName": "اسم الترويسة", @@ -6194,7 +6209,7 @@ "galadriel": "ربط Galadriel بمفتاح API.", "predibase": "رصيد تجريبي مجاني بقيمة 25 دولارًا (صلاحية لمدة 30 يومًا)", "chenzk": "بوابة متوافقة مع OpenAI مع كتالوج نماذج مباشر على chenzk.top.", - "freepik": "توليد الصور باستخدام واجهة برمجة تطبيقات Mystic من Freepik.", + "magnific": "توليد الصور باستخدام واجهة برمجة تطبيقات Mystic من Freepik.", "freetheai": "بوابة مجانية متوافقة مع OpenAI مع دعم نماذج التمرير المباشر (passthrough).", "g4f-gemini": "وكيل عكسي مجاني بدون مفتاح من g4f.space إلى Gemini، محدود بـ 5 طلبات في الدقيقة.", "g4f-groq": "وكيل عكسي مجاني بدون مفتاح من g4f.space إلى Groq، محدود بـ 5 طلبات في الدقيقة.", @@ -6209,6 +6224,7 @@ "claude": "ربط Claude Code باستخدام تدفق OAuth الحالي.", "cline": "ربط Cline باستخدام تدفق OAuth الحالي.", "cursor": "ربط Cursor IDE باستخدام تدفق OAuth الحالي.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "ربط GitHub Copilot باستخدام تدفق OAuth الحالي.", "gitlab-duo": "تطبيق OAuth بنطاقات ai_features + read_user. قم بتكوين GITLAB_DUO_OAUTH_CLIENT_ID واختياريًا GITLAB_DUO_OAUTH_CLIENT_SECRET على مثيل OmniRoute هذا.", "kilocode": "ربط Kilo Code باستخدام تدفق OAuth الحالي.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "في فترة تهدئة", "codexPoolUsed": "مُستخدم", "codexPoolUntil": "حتى {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "التراجع المجهول", "anonymousFallbackDesc": "عند استنفاد جميع الاتصالات المكونة (الحصة، الاعتمادات، أو انتهاء الصلاحية)، استخدم مؤقتًا المستوى بدون مفتاح لهذا المزود. قم بإيقاف التشغيل لتخطي هذا المزود بدلاً من إرسال طلبات مجهولة — يُوصى بذلك عندما يرفض المستوى بدون مفتاح هذه الطلبات (401).", "anonymousFallbackEnabled": "تم تمكين النسخة الاحتياطية المجهولة لـ {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "إعدادات نقطة نهاية النموذج المحفوظ", "searchByModelAria": "البحث حسب الطراز", "selectSupportedEndpoint": "اختر نقطة نهاية مدعومة واحدة على الأقل", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "تم تعطيل جلب النموذج العلوي تلقائيًا", - "autoFetchModels": "جلب النماذج من المصدر تلقائيًا", - "autoFetchModelsEnabled": "تم تمكين جلب النموذج العلوي تلقائيًا", - "autoFetchModelsTooltip": "استرجاع وتخزين نماذج المصدر عند الحاجة", - "autoFetchModelsToggleFailed": "فشل في تبديل جلب النموذج العلوي تلقائيًا", - "overridesUpstreamModelHint": "إعداداتك تتجاوز هذا النموذج العلوي", - "overridesUpstreamModel": "يتجاوز المصدر", - "autoFetchModelsPartialFailure": "تم تحديث بعض الاتصالات، لكن نموذج المصدر التلقائي لم يتغير في كل مكان", - "resetToUpstreamDefaults": "استعادة الإعدادات الافتراضية للمصدر", - "resetToUpstreamDefaultsSuccess": "تم استعادة إعدادات النموذج الافتراضية من المصدر", - "resetToUpstreamDefaultsFailed": "فشل في استعادة إعدادات النموذج الافتراضية من المصدر" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "الإعدادات", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "قم بوضع علامة على اتصالات الموفر على أنها معطلة بشكل دائم إذا أعادت إشارات حظر طرفية محددة (على سبيل المثال، HTTP 403 \"التحقق من حسابك\"). يؤدي هذا إلى إزالتها من دوران التحرير والسرد.", "autoDisableThreshold": "عتبة الحظر", "autoDisableThresholdDesc": "إشارات الحظر المتتالية مطلوبة قبل التعطيل الدائم.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "الكلمات المفتاحية المحظورة", "customBannedSignalsDesc": "كلمات مفتاحية إضافية تؤدي إلى اكتشاف حظر الحساب الدائم. تنطبق الكلمات المفتاحية المدمجة دائمًا.", "customBannedSignalsPlaceholder": "على سبيل المثال: api key revoked", @@ -7210,6 +7203,7 @@ "configured": "مُهيأ", "none": "بلا", "modelOverrideValuePlaceholder": "قيمة رقمية", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "إضافة مفتاح وقيمة", "noModelOverrides": "لم يتم تكوين أي تجاوزات لهذا النموذج.", "modelOverrideLoadFailed": "فشل تحميل تجاوزات النموذج", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK مقتضب (文言)", "description": "أسلوب صيني كلاسيكي فائق الاقتضاب (متاح للغة الصينية فقط)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "تنتقل مجموعات التناوب الدائري (Round-robin) والمجموعات العشوائية إلى اتصال مختلف في كل طلب بدلاً من تثبيت محادثة كاملة باتصال واحد بواسطة هاش الرسالة الأولى. اتركها معطلة للحفاظ على إصابات prompt-cache للمحادثات متعددة الأدوار. عمليات التجاوز الخاصة بكل مجموعة لها الأسبقية.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "تنقيح بيانات الاعتماد", "credentialRedactionDesc": "تنقيح مفاتيح API والرموز المميزة والأسرار من السياق المرسل إلى الموفرين ومن الاستجابات.", "enableCredentialRedaction": "تمكين تنقيح بيانات الاعتماد", @@ -8621,6 +8623,27 @@ }, "enableTitle": "تمكين المحرك", "enableDescription": "يعمل في نهاية المكدس (بعد أن يقوم RTK/Caveman بتنظيف النص، ويقوم OmniGlyph بتحويل المتبقي إلى صور) ويعمل أيضًا بشكل مستقل عبر وضع omniglyph. هذه نسخة معاينة وتظل معطلة افتراضيًا حتى تكتمل عملية التحقق الشاملة من البداية للنهاية.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "تم الحفظ.", "saveFailed": "تعذر الحفظ.", "enableAria": "تمكين محرك OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "أقصى", "grokAutoTopUpMonth": "شهر", "grokAdditionalCredits": "أرصدة إضافية", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "المسجل", "proxyTab": "بروكسي", "budgetManagement": "إدارة الميزانية", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "الرمز الأول", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 7c7754b05e..6bf2e21e55 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Konsol qeydləri", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Vizual tələb zaman cədvəli", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Qlobal marşrutlaşdırma", "mitmProxy": "MITM Proksi", "oneProxy": "1 Proksi", @@ -1291,9 +1293,7 @@ "open": "açıq", "close": "bağla" }, - "noResults": "Heç bir nəticə yoxdur", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Heç bir nəticə yoxdur" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kvota payı", "discovery": "Kəşf", "freeProviderRankings": "Pulsuz provayder reytinqləri", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Pulsuz səviyyələr", "gamification": "Oyunlaşdırma", "leaderboard": "Liderlər cədvəli", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 14 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "This provider has been deprecated", "riskNotice": { "title": "Davam etməzdən əvvəl", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "İstifadə xəbərdarlıqları olan provayder — ətraflı məlumat üçün klikləyin", "oauth": "Bu provayder proksi/router istifadəsi üçün icazə verilməyən rəsmi məhsul sessiyanızdan/OAuth-dan istifadə edir. İntensiv avtonom agent istifadəsini (OpenCloud tərzi, uzun çoxmərhələli axınlar, böyük paketlər) tövsiyə etmirik — upstream xidmət hesabı məhdudlaşdırmaqla və ya bloklamaqla reaksiya verə bilər. Riski öz üzərinizə götürərək istifadə edin.", "webCookie": "Bu provayder veb sessiya kukiləriniz vasitəsilə autentifikasiya edir. Upstream xidmət istənilən vaxt sessiyanı ləğv edə bilər və bu da yenidən daxil olmağınızı tələb edər. Uzunmüddətli nəzarətsiz əməliyyatlar üçün tövsiyə edilmir. Riski öz üzərinizə götürərək istifadə edin.", @@ -5107,9 +5111,9 @@ "cancel": "Ləğv et" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "Avtomatik olaraq yuxarı axın modellərini əldə et", + "autoFetchModelsTooltip": "Tələb olunduqda yuxarıdakı modelləri əldə et və keşlə.", + "autoFetchModelsEnabled": "Yuxarı axın modelinin avtomatik yüklənməsi aktivdir", + "autoFetchModelsDisabled": "Yuxarı axın modelinin avtomatik əldə edilməsi deaktivdir", + "autoFetchModelsToggleFailed": "Yuxarı axın modelinin avtomatik əldə edilməsini dəyişdirmək mümkün olmadı", + "autoFetchModelsPartialFailure": "Bəzi bağlantılar yeniləndi, lakin yuxarı axın modelinin avtomatik alınması hər yerdə dəyişdirilmədi", + "overridesUpstreamModel": "Yuxarıdan üstəgəl edir", + "overridesUpstreamModelHint": "Sizin parametrləriniz bu yuxarı axın modelini üstələyir", + "resetToUpstreamDefaults": "Yuxarı axın standartlarını bərpa et", + "resetToUpstreamDefaultsSuccess": "Yuxarı axın modelinin standart parametrləri bərpa edildi", + "resetToUpstreamDefaultsFailed": "Yuxarı axın modelinin standartlarını bərpa etmək mümkün olmadı", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Yerli web_fetch alət çağırışlarını OmniRoute-un /v1/web/fetch ünvanına yenidən yazın.", "interceptionLoadError": "Ələ keçirmə parametrlərini yükləmək mümkün olmadı: {error}", "interceptionSaveError": "Ələ keçirmə parametrlərini yadda saxlamaq mümkün olmadı: {error}", - "ccAliasSectionTitle": "Claude Kodunda (claude/…) açıq et", - "ccAliasSectionHint": "Bu təminatçının modellərini claude/<provider>/<model> güzgü ID-ləri altında reklam edin ki, Claude Code-un qapı modeli kəşfi onları siyahıya ala bilsin. Varsayılan olaraq deaktivdir — bunu aktivləşdirmək bütün müştərilər üçün kataloq girişlərini ikiqat artırır.", - "ccAliasProviderLevelLabel": "Təchizatçı standart", - "ccAliasModelOverridesLabel": "Model üzrə üst-üstə düşmələr", - "ccAliasModelOverrideAriaLabel": "{modelId} üçün üst-üstə düşmə", - "ccAliasStateInherit": "İrsiyyət", - "ccAliasStateOn": "Üstündə", - "ccAliasStateOff": "Söndürüldü", - "ccAliasAddModelPlaceholder": "Model id (məsələn, gpt-4o)", - "ccAliasAddModelButton": "Override əlavə et", - "ccAliasLoadError": "Kəşf-alias parametrlərini yükləmək mümkün olmadı: {error}", - "ccAliasSaveError": "discovery-alias parametrlərini saxlamaq mümkün olmadı: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Galadriel-i API açarı ilə qoşun.", "predibase": "$25 pulsuz sınaq krediti (30 günlük etibarlılıq müddəti)", "chenzk": "chenzk.top ünvanında canlı model kataloqu olan OpenAI ilə uyğun gələn şlüz.", - "freepik": "Freepik-in Mystic API-si ilə şəkillər yaradın.", + "magnific": "Freepik-in Mystic API-si ilə şəkillər yaradın.", "freetheai": "Passthrough model dəstəyi olan pulsuz OpenAI ilə uyğun gələn şlüz.", "g4f-gemini": "Gemini-yə pulsuz, açarsız g4f.space tərs proksisi, dəqiqədə 5 sorğu ilə məhdudlaşdırılıb.", "g4f-groq": "Groq-a pulsuz, açarsız g4f.space tərs proksisi, dəqiqədə 5 sorğu ilə məhdudlaşdırılıb.", @@ -6209,6 +6224,7 @@ "claude": "Claude Code-u mövcud OAuth axını ilə qoşun.", "cline": "Cline-ı mövcud OAuth axını ilə qoşun.", "cursor": "Cursor IDE-ni mövcud OAuth axını ilə qoşun.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "GitHub Copilot-u mövcud OAuth axını ilə qoşun.", "gitlab-duo": "ai_features + read_user əhatə dairələri olan OAuth tətbiqi. Bu OmniRoute instansiyasında GITLAB_DUO_OAUTH_CLIENT_ID və istəyə bağlı olaraq GITLAB_DUO_OAUTH_CLIENT_SECRET konfiqurasiya edin.", "kilocode": "Kilo Code-u mövcud OAuth axını ilə qoşun.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Gözləmə müddətindədir", "codexPoolUsed": "istifadə edilib", "codexPoolUntil": "{value} tarixinədək", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonim ehtiyat", "anonymousFallbackDesc": "Bütün konfiqurasiya olunmuş bağlantılar tükəndikdə (kvota, kreditlər və ya müddət), müvəqqəti olaraq bu təminatçının açarsız səviyyəsini istifadə edin. Anonim sorğular göndərmək əvəzinə bu təminatçını atlamaq üçün söndürün — açarsız səviyyə onları rədd etdikdə (401) tövsiyə olunur.", "anonymousFallbackEnabled": "{provider} üçün anonim ehtiyat aktivdir", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Saxlanmış model son nöqtəsi parametrləri", "searchByModelAria": "Model üzrə axtarış edin", "selectSupportedEndpoint": "Ən azı bir dəstəklənən son nöqtəni seçin", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Avtomatik olaraq yuxarı axın modellərini əldə et", - "autoFetchModelsDisabled": "Yuxarı axın modelinin avtomatik əldə edilməsi deaktivdir", - "autoFetchModelsTooltip": "Tələb olunduqda yuxarıdakı modelləri əldə et və keşlə.", - "autoFetchModelsEnabled": "Yuxarı axın modelinin avtomatik yüklənməsi aktivdir", - "autoFetchModelsToggleFailed": "Yuxarı axın modelinin avtomatik əldə edilməsini dəyişdirmək mümkün olmadı", - "overridesUpstreamModelHint": "Sizin parametrləriniz bu yuxarı axın modelini üstələyir", - "overridesUpstreamModel": "Yuxarıdan üstəgəl edir", - "autoFetchModelsPartialFailure": "Bəzi bağlantılar yeniləndi, lakin yuxarı axın modelinin avtomatik alınması hər yerdə dəyişdirilmədi", - "resetToUpstreamDefaults": "Yuxarı axın standartlarını bərpa et", - "resetToUpstreamDefaultsSuccess": "Yuxarı axın modelinin standart parametrləri bərpa edildi", - "resetToUpstreamDefaultsFailed": "Yuxarı axın modelinin standartlarını bərpa etmək mümkün olmadı" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Qadağan olunmuş açar sözlər", "customBannedSignalsDesc": "Hesabın daimi bloklanmasının aşkarlanmasını işə salan əlavə açar sözlər. Daxili açar sözlər həmişə tətbiq olunur.", "customBannedSignalsPlaceholder": "məs., api key revoked", @@ -7210,6 +7203,7 @@ "configured": "konfiqurasiya edilib", "none": "Heç biri", "modelOverrideValuePlaceholder": "Ədədi dəyər", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Açar-dəyər əlavə et", "noModelOverrides": "Bu model üçün heç bir yenidən təyinetmə konfiqurasiya edilməyib.", "modelOverrideLoadFailed": "Model yenidən təyinetmələrini yükləmək mümkün olmadı", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Yığcam CJK (文言)", "description": "Klassik Çin ultra-yığcam üslubu (yalnız Çin dili üçün əlçatandır)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin və təsadüfi kombinasiyalar, bütün söhbəti ilk mesajın heşinə görə bir bağlantıya bağlamaq əvəzinə, hər sorğuda fərqli bir bağlantıya keçid edir. Çoxmərhələli söhbətlər üçün prompt-cache hitlərini qorumaq üçün bunu söndürülmüş saxlayın. Hər kombinasiya üçün üstünlüklər prioritet təşkil edir.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Kimlik məlumatlarının gizlədilməsi", "credentialRedactionDesc": "Provayderlərə göndərilən kontekstdən və cavablardan API açarlarını, tokenləri və gizli məlumatları gizlədin.", "enableCredentialRedaction": "Kimlik məlumatlarının gizlədilməsini aktivləşdirin", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Mühərriki aktivləşdir", "enableDescription": "Yığında ən son işləyir (RTK/Caveman mətni təmizlədikdən sonra OmniGlyph qalan hissəni şəkillərə çevirir) və həmçinin omniglyph rejimi vasitəsilə müstəqil işləyir. Bu, ilkin baxışdır və başdan-başa yoxlama tamamlanana qədər standart olaraq söndürülmüş qalır.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Saxlanıldı.", "saveFailed": "Saxlamaq mümkün olmadı.", "enableAria": "OmniGlyph mühərrikini aktivləşdir", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksimum", "grokAutoTopUpMonth": "ay", "grokAdditionalCredits": "Əlavə Kreditlər", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "İlk Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 463943e63a..a8b6e486b3 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Визуален времеви график на заявките", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "отвори", "close": "затвори" }, - "noResults": "Няма резултати", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Няма резултати" }, "webhooks": { "title": "Уеб кукички", @@ -1739,8 +1739,8 @@ "quotaShare": "Споделяне на квота", "discovery": "Откриване", "freeProviderRankings": "Класации на безплатни доставчици", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Безплатни нива", "gamification": "Геймификация", "leaderboard": "Класация", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Този доставчик е отхвърлен", "riskNotice": { "title": "Преди да продължите", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Доставчик с предупреждения за употреба — щракнете за подробности", "oauth": "Този доставчик използва вашата официална продуктова сесия/OAuth, която не е оторизирана за използване като прокси/рутер. Не препоръчваме интензивно използване на автономни агенти (в стил OpenCloud, дълги многостъпкови процеси, големи партиди) — upstream услугата може да реагира чрез ограничаване или блокиране на акаунта. Използвайте на свой собствен риск.", "webCookie": "Този доставчик се удостоверява чрез бисквитките на вашата уеб сесия. Upstream услугата може да анулира сесията по всяко време, което ще изисква да се влезете отново. Не се препоръчва за дълги операции без надзор. Използвайте на свой собствен риск.", @@ -5107,9 +5111,9 @@ "cancel": "Отказ" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Забранено", "enableProvider": "Активиране на доставчика", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Пропускане на {count} съществуващи модела", "autoSync": "Автоматично синхронизиране", "autoSyncShort": "Синхронизиране", + "autoFetchModels": "Автоматично извличане на upstream модели", + "autoFetchModelsTooltip": "Изтеглете и кеширайте upstream модели, когато е необходимо", + "autoFetchModelsEnabled": "Автоматично извличане на upstream модела е активирано", + "autoFetchModelsDisabled": "Автоматично извличане на upstream модела е деактивирано", + "autoFetchModelsToggleFailed": "Неуспешно превключване на автоматично извличане на upstream модела", + "autoFetchModelsPartialFailure": "Някои връзки са актуализирани, но автоматичното извличане на upstream модела не беше променено навсякъде", + "overridesUpstreamModel": "Презаписва upstream", + "overridesUpstreamModelHint": "Вашите настройки надвиват тази основна модел.", + "resetToUpstreamDefaults": "Възстановяване на настройки по подразбиране на upstream", + "resetToUpstreamDefaultsSuccess": "Възстановени настройки по подразбиране на upstream модела", + "resetToUpstreamDefaultsFailed": "Неуспешно възстановяване на подразбиращите се настройки на upstream модела", "autoSyncTooltip": "Автоматично опресняване на списъка с модели на всеки 24 часа (може да се конфигурира чрез MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Автоматичното синхронизиране е активирано — моделите ще се опресняват периодично", "autoSyncDisabled": "Автоматичното синхронизиране е деактивирано", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Пренаписване на повикванията на вградения инструмент web_fetch към /v1/web/fetch на OmniRoute.", "interceptionLoadError": "Неуспешно зареждане на настройките за прихващане: {error}", "interceptionSaveError": "Неуспешно запазване на настройките за прихващане: {error}", - "ccAliasSectionTitle": "Изложи в Claude Code (claude/…)", - "ccAliasSectionHint": "Рекламирайте моделите на този доставчик под claude/<provider>/<model> mirror ids, за да може откритията на моделите на Claude Code да ги изброява. По подразбиране е изключено — активирането му удвоява записите в каталога за всички клиенти.", - "ccAliasProviderLevelLabel": "Дефолтен доставчик", - "ccAliasModelOverridesLabel": "Преодолявания на моделите", - "ccAliasModelOverrideAriaLabel": "Презапис за {modelId}", - "ccAliasStateInherit": "Наследи", - "ccAliasStateOn": "Включено", - "ccAliasStateOff": "Изключено", - "ccAliasAddModelPlaceholder": "Идентификатор на модела (напр. gpt-4o)", - "ccAliasAddModelButton": "Добави заместване", - "ccAliasLoadError": "Неуспешно зареждане на настройки за discovery-alias: {error}", - "ccAliasSaveError": "Неуспешно запазване на настройката discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Свържете Galadriel с API ключ.", "predibase": "$25 безплатни кредити за пробен период (валидност 30 дни)", "chenzk": "Съвместим с OpenAI шлюз с каталог на моделите на живо на chenzk.top.", - "freepik": "Генерирайте изображения с Mystic API на Freepik.", + "magnific": "Генерирайте изображения с Mystic API на Freepik.", "freetheai": "Безплатен съвместим с OpenAI шлюз с поддръжка на passthrough модели.", "g4f-gemini": "Безплатно обратно прокси без ключ от g4f.space към Gemini, ограничено до 5 заявки в минута.", "g4f-groq": "Безплатно обратно прокси без ключ от g4f.space към Groq, ограничено до 5 заявки в минута.", @@ -6209,6 +6224,7 @@ "claude": "Свържете Claude Code със съществуващия OAuth поток.", "cline": "Свържете Cline със съществуващия OAuth поток.", "cursor": "Свържете Cursor IDE със съществуващия OAuth поток.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Свържете GitHub Copilot със съществуващия OAuth поток.", "gitlab-duo": "OAuth приложение с обхвати ai_features + read_user. Конфигурирайте GITLAB_DUO_OAUTH_CLIENT_ID и по избор GITLAB_DUO_OAUTH_CLIENT_SECRET на тази инстанция на OmniRoute.", "kilocode": "Свържете Kilo Code със съществуващия OAuth поток.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "В период на изчакване", "codexPoolUsed": "използвано", "codexPoolUntil": "До {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Анонимен резервен вариант", "anonymousFallbackDesc": "Когато всички конфигурирани връзки са изчерпани (квота, кредити или изтичане), временно използвайте безключовия слой на този доставчик. Изключете, за да пропуснете този доставчик вместо да изпращате анонимни заявки — препоръчително, когато безключовият слой ги отхвърля (401).", "anonymousFallbackEnabled": "Анонимен резервен вариант е активиран за {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Настройки на крайна точка на запазен модел", "searchByModelAria": "Търсене по модел", "selectSupportedEndpoint": "Изберете поне една поддържана крайна точка", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Автоматично извличане на upstream модели", - "autoFetchModelsEnabled": "Автоматично извличане на upstream модела е активирано", - "autoFetchModelsTooltip": "Изтеглете и кеширайте upstream модели, когато е необходимо", - "overridesUpstreamModel": "Презаписва upstream", - "autoFetchModelsToggleFailed": "Неуспешно превключване на автоматично извличане на upstream модела", - "autoFetchModelsPartialFailure": "Някои връзки са актуализирани, но автоматичното извличане на upstream модела не беше променено навсякъде", - "autoFetchModelsDisabled": "Автоматично извличане на upstream модела е деактивирано", - "resetToUpstreamDefaults": "Възстановяване на настройки по подразбиране на upstream", - "resetToUpstreamDefaultsSuccess": "Възстановени настройки по подразбиране на upstream модела", - "resetToUpstreamDefaultsFailed": "Неуспешно възстановяване на подразбиращите се настройки на upstream модела", - "overridesUpstreamModelHint": "Вашите настройки надвиват тази основна модел." + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Настройки", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Забранени ключови думи", "customBannedSignalsDesc": "Допълнителни ключови думи, които задействат откриване за постоянен бан на акаунта. Вградените ключови думи се прилагат винаги.", "customBannedSignalsPlaceholder": "напр. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "конфигуриран", "none": "Няма", "modelOverrideValuePlaceholder": "Числова стойност", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Добавяне на ключ-стойност", "noModelOverrides": "Няма конфигурирани предефинирания за този модел.", "modelOverrideLoadFailed": "Неуспешно зареждане на предефиниранията на модела", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Сбит CJK (文言)", "description": "Класически китайски ултра-сбит стил (наличен само за китайски)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Комбинациите от тип Round-robin и random се ротират към различна връзка при всяка заявка, вместо да обвързват целия разговор към една връзка чрез хеша на първото съобщение. Оставете изключено, за да запазите съвпаденията в кеша на подканите (prompt-cache) за многостъпкови чатове. Персонализираните настройки за всяка комбинация имат предимство.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Скриване на идентификационни данни", "credentialRedactionDesc": "Скриване на API ключове, токени и тайни от контекста, изпратен към доставчиците, и от отговорите.", "enableCredentialRedaction": "Активиране на скриването на идентификационни данни", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Активиране на енджина", "enableDescription": "Изпълнява се последен в стека (след като RTK/Caveman изчисти текста, OmniGlyph конвертира остатъка в изображения) и също така работи самостоятелно чрез режим omniglyph. Това е предварителна версия и остава изключена по подразбиране, докато не приключи цялостната валидация.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Запазено.", "saveFailed": "Неуспешно запазване.", "enableAria": "Активиране на енджина OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "макс", "grokAutoTopUpMonth": "месец", "grokAdditionalCredits": "Допълнителни кредити", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Дървосекач", "proxyTab": "Прокси", "budgetManagement": "Управление на бюджета", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Първи токен", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index fe486a3ffd..aadc026d06 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "ভিজ্যুয়াল রিকোয়েস্ট টাইমলাইন", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "খুলুন", "close": "বন্ধ করুন" }, - "noResults": "কোন ফলাফল নেই", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "কোন ফলাফল নেই" }, "webhooks": { "title": "ওয়েবহুক", @@ -1739,8 +1739,8 @@ "quotaShare": "কোটা শেয়ার", "discovery": "ডিসকভারি", "freeProviderRankings": "ফ্রি প্রোভাইডার র‍্যাঙ্কিং", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "ফ্রি টিয়ার", "gamification": "গেমিফিকেশন", "leaderboard": "লিডারবোর্ড", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "এই প্রদানকারীকে অবমূল্যায়ন করা হয়েছে", "riskNotice": { "title": "এগিয়ে যাওয়ার আগে", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "ব্যবহারের সতর্কতা সহ প্রদানকারী — বিস্তারিত জানতে ক্লিক করুন", "oauth": "এই প্রদানকারীটি আপনার অফিসিয়াল প্রোডাক্ট সেশন/OAuth ব্যবহার করে, যা প্রক্সি/রাউটার ব্যবহারের জন্য অনুমোদিত নয়। আমরা নিবিড় স্বায়ত্তশাসিত এজেন্ট ব্যবহার (OpenCloud-স্টাইল, দীর্ঘ বহু-ধাপের ফ্লো, বড় ব্যাচ) সুপারিশ করি না — আপস্ট্রিম অ্যাকাউন্টটি সীমাবদ্ধ বা নিষিদ্ধ করে প্রতিক্রিয়া জানাতে পারে। নিজের ঝুঁকিতে ব্যবহার করুন।", "webCookie": "এই প্রদানকারীটি আপনার ওয়েব সেশন কুকিজের মাধ্যমে প্রমাণীকরণ করে। আপস্ট্রিম পরিষেবাটি যেকোনো সময় সেশনটি বাতিল করতে পারে, যার ফলে আপনাকে আবার লগ ইন করতে হবে। দীর্ঘ সময় ধরে অযত্নে রেখে কাজ চালানোর জন্য প্রস্তাবিত নয়। নিজের ঝুঁকিতে ব্যবহার করুন।", @@ -5107,9 +5111,9 @@ "cancel": "বাতিল করুন" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "আপস্ট্রিম মডেলগুলি স্বয়ংক্রিয়ভাবে আনুন", + "autoFetchModelsTooltip": "প্রয়োজন হলে আপস্ট্রিম মডেলগুলি ফেচ এবং ক্যাশ করুন", + "autoFetchModelsEnabled": "আপস্ট্রিম মডেল স্বয়ংক্রিয়-ফেচ সক্ষম করা হয়েছে", + "autoFetchModelsDisabled": "আপস্ট্রিম মডেল অটো-ফেচ নিষ্ক্রিয় করা হয়েছে", + "autoFetchModelsToggleFailed": "আপস্ট্রিম মডেল অটো-ফেচ টগল করতে ব্যর্থ হয়েছে", + "autoFetchModelsPartialFailure": "কিছু সংযোগ আপডেট হয়েছে, কিন্তু আপস্ট্রিম মডেলের স্বয়ংক্রিয়-ফেচ সব জায়গায় পরিবর্তিত হয়নি", + "overridesUpstreamModel": "আপস্ট্রিম ওভাররাইডস", + "overridesUpstreamModelHint": "আপনার সেটিংস এই আপস্ট্রিম মডেলকে অতিক্রম করে", + "resetToUpstreamDefaults": "আপস্ট্রিম ডিফল্টগুলি পুনরুদ্ধার করুন", + "resetToUpstreamDefaultsSuccess": "আপস্ট্রিম মডেল ডিফল্টগুলি পুনরুদ্ধার করা হয়েছে", + "resetToUpstreamDefaultsFailed": "আপস্ট্রিম মডেল ডিফল্টগুলি পুনরুদ্ধার করতে ব্যর্থ হয়েছে", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "নেটিভ web_fetch টুল কলগুলিকে OmniRoute-এর /v1/web/fetch-এ রিরাইট করুন।", "interceptionLoadError": "ইন্টারসেপশন সেটিংস লোড করতে ব্যর্থ হয়েছে: {error}", "interceptionSaveError": "ইন্টারসেপশন সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে: {error}", - "ccAliasSectionTitle": "Claude কোডে প্রকাশ করুন (claude/…)", - "ccAliasSectionHint": "এই প্রদানকারীর মডেলগুলি claude/<provider>/<model> মিরর আইডির অধীনে বিজ্ঞাপন দিন যাতে Claude Code-এর গেটওয়ে মডেল আবিষ্কার সেগুলি তালিকাভুক্ত করতে পারে। ডিফল্টভাবে বন্ধ — এটি সক্ষম করা হলে সমস্ত ক্লায়েন্টের জন্য ক্যাটালগের এন্ট্রি দ্বিগুণ হয়।", - "ccAliasProviderLevelLabel": "প্রদানকারী ডিফল্ট", - "ccAliasModelOverridesLabel": "প্রতি-মডেল ওভাররাইডস", - "ccAliasModelOverrideAriaLabel": "{modelId} এর জন্য ওভাররাইড", - "ccAliasStateInherit": "উত্তরাধিকারী", - "ccAliasStateOn": "চালু", - "ccAliasStateOff": "বন্ধ", - "ccAliasAddModelPlaceholder": "মডেল আইডি (যেমন gpt-4o)", - "ccAliasAddModelButton": "অভাররাইড যোগ করুন", - "ccAliasLoadError": "ডিসকভারি-অ্যালিয়াস সেটিংস লোড করতে ব্যর্থ: {error}", - "ccAliasSaveError": "ডিসকভারি-অ্যালিয়াস সেটিং সংরক্ষণ করতে ব্যর্থ: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "একটি API কী দিয়ে Galadriel সংযুক্ত করুন।", "predibase": "$25 ফ্রি ট্রায়াল ক্রেডিট (30 দিনের মেয়াদ)", "chenzk": "chenzk.top-এ লাইভ মডেল ক্যাটালগ সহ OpenAI-সামঞ্জস্যপূর্ণ গেটওয়ে।", - "freepik": "Freepik-এর Mystic API দিয়ে ছবি তৈরি করুন।", + "magnific": "Freepik-এর Mystic API দিয়ে ছবি তৈরি করুন।", "freetheai": "passthrough মডেল সমর্থন সহ বিনামূল্যের OpenAI-সামঞ্জস্যপূর্ণ গেটওয়ে।", "g4f-gemini": "Gemini-এর জন্য বিনামূল্যের কী-বিহীন g4f.space রিভার্স প্রক্সি, প্রতি মিনিটে 5টি অনুরোধে সীমাবদ্ধ।", "g4f-groq": "Groq-এর জন্য বিনামূল্যের কী-বিহীন g4f.space রিভার্স প্রক্সি, প্রতি মিনিটে 5টি অনুরোধে সীমাবদ্ধ।", @@ -6209,6 +6224,7 @@ "claude": "বিদ্যমান OAuth ফ্লো দিয়ে Claude Code সংযুক্ত করুন।", "cline": "বিদ্যমান OAuth ফ্লো দিয়ে Cline সংযুক্ত করুন।", "cursor": "বিদ্যমান OAuth ফ্লো দিয়ে Cursor IDE সংযুক্ত করুন।", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "বিদ্যমান OAuth ফ্লো দিয়ে GitHub Copilot সংযুক্ত করুন।", "gitlab-duo": "ai_features + read_user স্কোপ সহ OAuth অ্যাপ্লিকেশন। এই OmniRoute ইনস্ট্যান্সে GITLAB_DUO_OAUTH_CLIENT_ID এবং ঐচ্ছিকভাবে GITLAB_DUO_OAUTH_CLIENT_SECRET কনফিগার করুন।", "kilocode": "বিদ্যমান OAuth ফ্লো দিয়ে Kilo Code সংযুক্ত করুন।", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "কুলডাউনে আছে", "codexPoolUsed": "ব্যবহৃত", "codexPoolUntil": "{value} পর্যন্ত", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "অজ্ঞাত ফালব্যাক", "anonymousFallbackDesc": "যখন সমস্ত কনফিগার করা সংযোগ শেষ হয়ে যায় (কোটা, ক্রেডিট, বা মেয়াদ শেষ), এই প্রদানকারীর কীবিহীন স্তরটি অস্থায়ীভাবে ব্যবহার করুন। অজ্ঞাত অনুরোধ পাঠানোর পরিবর্তে এই প্রদানকারীটি বাদ দিতে বন্ধ করুন — যখন কীবিহীন স্তর সেগুলি প্রত্যাখ্যান করে (401) তখন এটি সুপারিশ করা হয়।", "anonymousFallbackEnabled": "{provider} এর জন্য অজ্ঞাত ফFallback সক্রিয় করা হয়েছে", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "সংরক্ষিত মডেল এন্ডপয়েন্ট সেটিংস", "searchByModelAria": "মডেল দ্বারা অনুসন্ধান করুন", "selectSupportedEndpoint": "কমপক্ষে একটি সমর্থিত এন্ডপয়েন্ট নির্বাচন করুন", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsTooltip": "প্রয়োজন হলে আপস্ট্রিম মডেলগুলি ফেচ এবং ক্যাশ করুন", - "autoFetchModelsEnabled": "আপস্ট্রিম মডেল স্বয়ংক্রিয়-ফেচ সক্ষম করা হয়েছে", - "autoFetchModelsDisabled": "আপস্ট্রিম মডেল অটো-ফেচ নিষ্ক্রিয় করা হয়েছে", - "autoFetchModels": "আপস্ট্রিম মডেলগুলি স্বয়ংক্রিয়ভাবে আনুন", - "overridesUpstreamModel": "আপস্ট্রিম ওভাররাইডস", - "autoFetchModelsToggleFailed": "আপস্ট্রিম মডেল অটো-ফেচ টগল করতে ব্যর্থ হয়েছে", - "autoFetchModelsPartialFailure": "কিছু সংযোগ আপডেট হয়েছে, কিন্তু আপস্ট্রিম মডেলের স্বয়ংক্রিয়-ফেচ সব জায়গায় পরিবর্তিত হয়নি", - "overridesUpstreamModelHint": "আপনার সেটিংস এই আপস্ট্রিম মডেলকে অতিক্রম করে", - "resetToUpstreamDefaults": "আপস্ট্রিম ডিফল্টগুলি পুনরুদ্ধার করুন", - "resetToUpstreamDefaultsFailed": "আপস্ট্রিম মডেল ডিফল্টগুলি পুনরুদ্ধার করতে ব্যর্থ হয়েছে", - "resetToUpstreamDefaultsSuccess": "আপস্ট্রিম মডেল ডিফল্টগুলি পুনরুদ্ধার করা হয়েছে" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "নিষিদ্ধ কীওয়ার্ড", "customBannedSignalsDesc": "অতিরিক্ত কীওয়ার্ড যা স্থায়ী অ্যাকাউন্ট ব্যান সনাক্তকরণ ট্রিগার করে। বিল্ট-ইন কীওয়ার্ড সর্বদা প্রযোজ্য।", "customBannedSignalsPlaceholder": "যেমন: api key revoked", @@ -7210,6 +7203,7 @@ "configured": "কনফিগার করা হয়েছে", "none": "কোনোটিই নয়", "modelOverrideValuePlaceholder": "সংখ্যাসূচক মান", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "কী ভ্যালু যোগ করুন", "noModelOverrides": "এই মডেলের জন্য কোনো ওভাররাইড কনফিগার করা হয়নি।", "modelOverrideLoadFailed": "মডেল ওভাররাইড লোড করতে ব্যর্থ হয়েছে", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "সংক্ষিপ্ত CJK (文言)", "description": "ক্লাসিক্যাল-চাইনিজ অতি-সংক্ষিপ্ত শৈলী (শুধুমাত্র চাইনিজ ভাষার জন্য উপলব্ধ)।" @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "রাউন্ড-রবিন এবং র‍্যান্ডম কম্বোগুলি প্রথম বার্তার হ্যাশ দ্বারা একটি সম্পূর্ণ কথোপকথনকে একটি সংযোগে পিন করার পরিবর্তে প্রতিটি অনুরোধে একটি ভিন্ন সংযোগে রোটেট করে। মাল্টি-টার্ন চ্যাটের জন্য প্রম্পট-ক্যাশ হিট সংরক্ষণ করতে এটি বন্ধ রাখুন। প্রতি-কম্বো ওভাররাইড অগ্রাধিকার পাবে।", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "ক্রেডেনশিয়াল রিডাকশন", "credentialRedactionDesc": "প্রোভাইডারদের কাছে পাঠানো কনটেক্সট এবং প্রতিক্রিয়া থেকে API কী, টোকেন এবং সিক্রেট রিডাক্ট করুন।", "enableCredentialRedaction": "ক্রেডেনশিয়াল রিডাকশন সক্রিয় করুন", @@ -8621,6 +8623,27 @@ }, "enableTitle": "ইঞ্জিনটি সক্রিয় করুন", "enableDescription": "স্ট্যাকের সবার শেষে চলে (RTK/Caveman টেক্সট পরিষ্কার করার পর, OmniGlyph বাকি অংশকে ইমেজে রূপান্তর করে) এবং omniglyph মোডের মাধ্যমে এককভাবেও চলে। এটি একটি প্রিভিউ এবং এন্ড-টু-এন্ড যাচাইকরণ সম্পন্ন না হওয়া পর্যন্ত ডিফল্টরূপে বন্ধ থাকবে।", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "সংরক্ষিত হয়েছে।", "saveFailed": "সংরক্ষণ করা যায়নি।", "enableAria": "OmniGlyph ইঞ্জিনটি সক্রিয় করুন", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "সর্বাধিক", "grokAutoTopUpMonth": "মাস", "grokAdditionalCredits": "অতিরিক্ত ক্রেডিটস", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "প্রথম টোকেন", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 22139ce32a..5e95c8f357 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Vizualizace časové osy požadavků", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "otevřít", "close": "zavřít" }, - "noResults": "Žádné výsledky", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Žádné výsledky" }, "webhooks": { "title": "Webhooky", @@ -1739,8 +1739,8 @@ "quotaShare": "Podíl kvóty", "discovery": "Průzkum", "freeProviderRankings": "Žebříčky bezplatných poskytovatelů", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Bezplatné tarify", "gamification": "Gamifikace", "leaderboard": "Žebříček", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Podpora tohoto poskytovatele byla ukončena", "riskNotice": { "title": "Než budete pokračovat", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Poskytovatel s upozorněními k použití — klikněte pro podrobnosti", "oauth": "Tento poskytovatel používá vaši oficiální relaci/OAuth produktu, což není autorizováno pro použití jako proxy/router. Nedoporučujeme intenzivní používání autonomních agentů (styl OpenCloud, dlouhé vícekrokové toky, velké dávky) — upstream může reagovat omezením nebo zablokováním účtu. Používejte na vlastní riziko.", "webCookie": "Tento poskytovatel se autentizuje pomocí souborů cookie vaší webové relace. Služba upstream může relaci kdykoli zneplatnit, což bude vyžadovat opětovné přihlášení. Nedoporučuje se pro dlouhé bezobslužné operace. Používejte na vlastní riziko.", @@ -5107,9 +5111,9 @@ "cancel": "Zrušit" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Zakázáno", "enableProvider": "Povolit poskytovatele", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Přeskakování {count} existujících modelů", "autoSync": "Automatická synchronizace", "autoSyncShort": "Synchronizace", + "autoFetchModels": "Automaticky načíst modely z upstreamu", + "autoFetchModelsTooltip": "Načíst a uložit upstream modely, když je to potřeba", + "autoFetchModelsEnabled": "Automatické načítání modelu upstream je povoleno", + "autoFetchModelsDisabled": "Automatické načítání modelu upstream je zakázáno", + "autoFetchModelsToggleFailed": "Nepodařilo se přepnout automatické načítání modelu upstream", + "autoFetchModelsPartialFailure": "Některé připojení byly aktualizovány, ale automatické načítání modelu upstream nebylo změněno všude", + "overridesUpstreamModel": "Přepisuje upstream", + "overridesUpstreamModelHint": "Vaše nastavení přepisují tento upstream model", + "resetToUpstreamDefaults": "Obnovit výchozí hodnoty upstream", + "resetToUpstreamDefaultsSuccess": "Obnoveny výchozí modely upstream", + "resetToUpstreamDefaultsFailed": "Nepodařilo se obnovit výchozí hodnoty modelu upstream", "autoSyncTooltip": "Automaticky obnovuje seznam modelů každých 24 hodin (lze nastavit přes MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatická synchronizace povolena – modely se budou pravidelně obnovovat", "autoSyncDisabled": "Automatická synchronizace zakázána", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Přepisovat nativní volání nástroje web_fetch na /v1/web/fetch v OmniRoute.", "interceptionLoadError": "Nepodařilo se načíst nastavení zachytávání: {error}", "interceptionSaveError": "Nepodařilo se uložit nastavení zachytávání: {error}", - "ccAliasSectionTitle": "Expose v Claude Code (claude/…)", - "ccAliasSectionHint": "Inzerujte modely tohoto poskytovatele pod claude/<provider>/<model> zrcadlovými ID, aby mohl objevovací model brány Claude Code je zobrazit. Ve výchozím nastavení vypnuto — povolení tohoto zdvojnásobí záznamy v katalogu pro všechny klienty.", - "ccAliasProviderLevelLabel": "Výchozí poskytovatel", - "ccAliasModelOverridesLabel": "Přepsání na úrovni modelu", - "ccAliasModelOverrideAriaLabel": "Přepsání pro {modelId}", - "ccAliasStateInherit": "Dědit", - "ccAliasStateOn": "Zapnuto", - "ccAliasStateOff": "Vypnuto", - "ccAliasAddModelPlaceholder": "ID modelu (např. gpt-4o)", - "ccAliasAddModelButton": "Přidat přepsání", - "ccAliasLoadError": "Nepodařilo se načíst nastavení discovery-alias: {error}", - "ccAliasSaveError": "Nepodařilo se uložit nastavení discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream hlavičky", "compatUpstreamHeadersHint": "Nastavení s vysokými oprávněními — stejná úroveň důvěryhodnosti jako při úpravách přihlašovacích údajů API poskytovatele; měli by jej používat pouze důvěryhodní administrátoři. Sloučeno poté, co OmniRoute přidá ověření z klíče API poskytovatele. Pokud vlastní záhlaví používá stejný název jako existující (např. Authorization), vaše hodnota zcela nahradí automaticky vygenerované záhlaví (včetně tokenu Bearer) — upstream vidí pouze to, co jste zadali, nikoli klíč z nastavení. Nesprávná nastavení může způsobit chybu 401 nebo nefunkční upstream ověření. Jeden řádek na jedno záhlaví (např. extra ověření pro některé brány). Pro náhled najděte na hodnotu nebo ji označte. Uloží se při odklonu, kliknutí mimo nebo zavření tohoto panelu.", "compatUpstreamHeaderName": "Název hlavičky", @@ -6194,7 +6209,7 @@ "galadriel": "Připojte Galadriel pomocí API klíče.", "predibase": "Bezplatný zkušební kredit 25 $ (platnost 30 dní)", "chenzk": "Brána kompatibilní s OpenAI s živým katalogem modelů na chenzk.top.", - "freepik": "Generujte obrázky pomocí Mystic API od Freepik.", + "magnific": "Generujte obrázky pomocí Mystic API od Freepik.", "freetheai": "Bezplatná brána kompatibilní s OpenAI s podporou passthrough modelů.", "g4f-gemini": "Bezplatná reverzní proxy g4f.space bez klíče pro Gemini, omezená na 5 požadavků za minutu.", "g4f-groq": "Bezplatná reverzní proxy g4f.space bez klíče pro Groq, omezená na 5 požadavků za minutu.", @@ -6209,6 +6224,7 @@ "claude": "Připojte Claude Code pomocí stávajícího toku OAuth.", "cline": "Připojte Cline pomocí stávajícího toku OAuth.", "cursor": "Připojte Cursor IDE pomocí stávajícího toku OAuth.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Připojte GitHub Copilot pomocí stávajícího toku OAuth.", "gitlab-duo": "Aplikace OAuth s rozsahy (scopes) ai_features + read_user. Nakonfigurujte GITLAB_DUO_OAUTH_CLIENT_ID a volitelně GITLAB_DUO_OAUTH_CLIENT_SECRET na této instanci OmniRoute.", "kilocode": "Připojte Kilo Code pomocí stávajícího toku OAuth.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Probíhá čekací lhůta", "codexPoolUsed": "využito", "codexPoolUntil": "Do {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonymní záložní řešení", "anonymousFallbackDesc": "Když jsou všechny nakonfigurované připojení vyčerpány (kvóta, kredity nebo expirace), dočasně použijte bezklíčovou úroveň tohoto poskytovatele. Vypněte, abyste tohoto poskytovatele přeskočili místo odesílání anonymních požadavků — doporučeno, když bezklíčová úroveň je odmítá (401).", "anonymousFallbackEnabled": "Anonymní záložní možnost povolena pro {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Nastavení koncového bodu uloženého modelu", "searchByModelAria": "Hledat podle modelu", "selectSupportedEndpoint": "Vyberte alespoň jeden podporovaný koncový bod", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Automaticky načíst modely z upstreamu", - "autoFetchModelsTooltip": "Načíst a uložit upstream modely, když je to potřeba", - "autoFetchModelsDisabled": "Automatické načítání modelu upstream je zakázáno", - "autoFetchModelsEnabled": "Automatické načítání modelu upstream je povoleno", - "overridesUpstreamModel": "Přepisuje upstream", - "autoFetchModelsToggleFailed": "Nepodařilo se přepnout automatické načítání modelu upstream", - "overridesUpstreamModelHint": "Vaše nastavení přepisují tento upstream model", - "autoFetchModelsPartialFailure": "Některé připojení byly aktualizovány, ale automatické načítání modelu upstream nebylo změněno všude", - "resetToUpstreamDefaultsSuccess": "Obnoveny výchozí modely upstream", - "resetToUpstreamDefaultsFailed": "Nepodařilo se obnovit výchozí hodnoty modelu upstream", - "resetToUpstreamDefaults": "Obnovit výchozí hodnoty upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Nastavení", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Trvale označí připojení poskytovatele jako deaktivovaná, pokud vrátí specifické signály zablokování (např. HTTP 403 'verify your account'). Tím je odstraní z rotace komb.", "autoDisableThreshold": "Prahová hodnota zablokování", "autoDisableThresholdDesc": "Počet po sobě jdoucích signálů zablokování před trvalou deaktivací.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Zakázaná klíčová slova", "customBannedSignalsDesc": "Další klíčová slova, která spouštějí detekci trvalého zablokování účtu. Vestavěná klíčová slova platí vždy.", "customBannedSignalsPlaceholder": "např. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "nakonfigurováno", "none": "Žádné", "modelOverrideValuePlaceholder": "Číselná hodnota", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Přidat klíč-hodnotu", "noModelOverrides": "Pro tento model nejsou nakonfigurována žádná přepsání.", "modelOverrideLoadFailed": "Nepodařilo se načíst přepsání modelů", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Stručné CJK (文言)", "description": "Klasický čínský ultra stručný styl (k dispozici pouze pro čínštinu)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Kombinace round-robin a náhodného výběru rotují na jiné připojení při každém požadavku, místo aby připnuly celou konverzaci k jednomu připojení podle hashe první zprávy. Ponechte vypnuté, chcete-li zachovat zásahy v mezipaměti promptů pro vícekrokové chaty. Přepsání pro jednotlivé kombinace mají přednost.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Redigování přihlašovacích údajů", "credentialRedactionDesc": "Redigovat klíče API, tokeny a tajné klíče z kontextu odesílaného poskytovatelům a z odpovědí.", "enableCredentialRedaction": "Povolit redigování přihlašovacích údajů", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Povolit engine", "enableDescription": "Spouští se jako poslední v zásobníku (poté, co RTK/Caveman vyčistí text a OmniGlyph převede zbytek na obrázky) a běží také samostatně v režimu omniglyph. Toto je náhled a ve výchozím nastavení zůstává vypnutý, dokud nebude dokončeno end-to-end ověření.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Uloženo.", "saveFailed": "Nepodařilo se uložit.", "enableAria": "Povolit engine OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "měsíc", "grokAdditionalCredits": "Další kredity", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Zapisovač", "proxyTab": "Proxy", "budgetManagement": "Správa rozpočtu", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "První token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index cbd74f9a49..fdc9a5fc89 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visuel anmodnings tidslinje", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "åben", "close": "luk" }, - "noResults": "Ingen resultater", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Ingen resultater" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kvoteandel", "discovery": "Opdagelse", "freeProviderRankings": "Rangliste over gratis udbydere", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Gratis niveauer", "gamification": "Gamificering", "leaderboard": "Leaderboard", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Denne udbyder er blevet udfaset", "riskNotice": { "title": "Før du fortsætter", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Udbyder med forbehold for brug — klik for detaljer", "oauth": "Denne udbyder bruger din officielle produktsession/OAuth, som ikke er godkendt til proxy-/routerbrug. Vi anbefaler ikke intensiv brug af autonome agenter (OpenCloud-stil, lange flertrinsforløb, store batches) — upstream-tjenesten kan reagere ved at begrænse eller spærre kontoen. Brug på eget ansvar.", "webCookie": "Denne udbyder godkender via dine websessionscookies. Upstream-tjenesten kan til enhver tid gøre sessionen ugyldig, hvilket kræver, at du logger ind igen. Anbefales ikke til lange uovervågede handlinger. Brug på eget ansvar.", @@ -5107,9 +5111,9 @@ "cancel": "Annuller" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Deaktiveret", "enableProvider": "Aktiver udbyder", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Springer {count} eksisterende modeller over", "autoSync": "Auto-synkronisering", "autoSyncShort": "Synkronisering", + "autoFetchModels": "Auto-hent upstream modeller", + "autoFetchModelsTooltip": "Hent og cache upstream-modeller, når det er nødvendigt", + "autoFetchModelsEnabled": "Opstrømsmodel auto-hentning aktiveret", + "autoFetchModelsDisabled": "Opstrømsmodel auto-hentning deaktiveret", + "autoFetchModelsToggleFailed": "Mislykkedes at skifte upstream model auto-fetch", + "autoFetchModelsPartialFailure": "Nogle forbindelser blev opdateret, men upstream-model auto-fetch blev ikke ændret overalt", + "overridesUpstreamModel": "Overskriver upstream", + "overridesUpstreamModelHint": "Dine indstillinger overskriver denne upstream-model", + "resetToUpstreamDefaults": "Gendan upstream standardindstillinger", + "resetToUpstreamDefaultsSuccess": "Gendannet upstream model standardindstillinger", + "resetToUpstreamDefaultsFailed": "Mislykkedes med at gendanne standardindstillinger for upstream-modellen", "autoSyncTooltip": "Opdater modellisten automatisk hver 24. time (kan konfigureres via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatisk synkronisering aktiveret - modellerne opdateres med jævne mellemrum", "autoSyncDisabled": "Automatisk synkronisering deaktiveret", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Omskriv oprindelige web_fetch-værktøjskald til OmniRoutes /v1/web/fetch.", "interceptionLoadError": "Kunne ikke indlæse indstillinger for opsnapning: {error}", "interceptionSaveError": "Kunne ikke gemme indstillinger for opsnapning: {error}", - "ccAliasSectionTitle": "Eksponer i Claude Code (claude/…)", - "ccAliasSectionHint": "Reklamer for denne udbyders modeller under claude/<provider>/<model> spejl-id'er, så Claude Code's gateway modelopdagelse kan liste dem. Slået fra som standard — aktivering af dette fordobler katalogindgange for alle klienter.", - "ccAliasProviderLevelLabel": "Udbyder standard", - "ccAliasModelOverridesLabel": "Per-model overskrivninger", - "ccAliasModelOverrideAriaLabel": "Overskrivning for {modelId}", - "ccAliasStateInherit": "Arv", - "ccAliasStateOn": "Tændt", - "ccAliasStateOff": "Slukket", - "ccAliasAddModelPlaceholder": "Model id (f.eks. gpt-4o)", - "ccAliasAddModelButton": "Tilføj overskrivning", - "ccAliasLoadError": "Kunne ikke indlæse discovery-alias indstillinger: {error}", - "ccAliasSaveError": "Fejl ved gemning af discovery-alias indstilling: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Forbind Galadriel med en API-nøgle.", "predibase": "$25 gratis prøvekredit (30 dages gyldighed)", "chenzk": "OpenAI-kompatibel gateway med et live modelkatalog på chenzk.top.", - "freepik": "Generer billeder med Freepiks Mystic API.", + "magnific": "Generer billeder med Freepiks Mystic API.", "freetheai": "Gratis OpenAI-kompatibel gateway med understøttelse af passthrough-modeller.", "g4f-gemini": "Gratis nøglefri g4f.space reverse proxy til Gemini, begrænset til 5 anmodninger pr. minut.", "g4f-groq": "Gratis nøglefri g4f.space reverse proxy til Groq, begrænset til 5 anmodninger pr. minut.", @@ -6209,6 +6224,7 @@ "claude": "Forbind Claude Code med det eksisterende OAuth-flow.", "cline": "Forbind Cline med det eksisterende OAuth-flow.", "cursor": "Forbind Cursor IDE med det eksisterende OAuth-flow.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Forbind GitHub Copilot med det eksisterende OAuth-flow.", "gitlab-duo": "OAuth-applikation med ai_features + read_user-scopes. Konfigurer GITLAB_DUO_OAUTH_CLIENT_ID og valgfrit GITLAB_DUO_OAUTH_CLIENT_SECRET på denne OmniRoute-instans.", "kilocode": "Forbind Kilo Code med det eksisterende OAuth-flow.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "I nedkølingsperiode", "codexPoolUsed": "brugt", "codexPoolUntil": "Indtil {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonym fallback", "anonymousFallbackDesc": "Når alle konfigurerede forbindelser er udtømt (kvote, kreditter eller udløb), brug midlertidigt denne udbyders nøgleløse niveau. Sluk for at springe denne udbyder over i stedet for at sende anonyme anmodninger - anbefales når det nøgleløse niveau afviser dem (401).", "anonymousFallbackEnabled": "Anonym fallback aktiveret for {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Indstillinger for gemt model endpoint", "searchByModelAria": "Søg efter model", "selectSupportedEndpoint": "Vælg mindst én understøttet endpoint", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Opstrømsmodel auto-hentning deaktiveret", - "autoFetchModelsTooltip": "Hent og cache upstream-modeller, når det er nødvendigt", - "autoFetchModels": "Auto-hent upstream modeller", - "autoFetchModelsEnabled": "Opstrømsmodel auto-hentning aktiveret", - "autoFetchModelsToggleFailed": "Mislykkedes at skifte upstream model auto-fetch", - "autoFetchModelsPartialFailure": "Nogle forbindelser blev opdateret, men upstream-model auto-fetch blev ikke ændret overalt", - "overridesUpstreamModelHint": "Dine indstillinger overskriver denne upstream-model", - "overridesUpstreamModel": "Overskriver upstream", - "resetToUpstreamDefaultsSuccess": "Gendannet upstream model standardindstillinger", - "resetToUpstreamDefaultsFailed": "Mislykkedes med at gendanne standardindstillinger for upstream-modellen", - "resetToUpstreamDefaults": "Gendan upstream standardindstillinger" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Indstillinger", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Blokerede nøgleord", "customBannedSignalsDesc": "Yderligere nøgleord, der udløser registrering af permanent kontoudelukkelse. Indbyggede nøgleord gælder altid.", "customBannedSignalsPlaceholder": "f.eks. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "konfigureret", "none": "Ingen", "modelOverrideValuePlaceholder": "Numerisk værdi", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Tilføj nøgleværdi", "noModelOverrides": "Ingen tilsidesættelser konfigureret for denne model.", "modelOverrideLoadFailed": "Kunne ikke indlæse modeltilsidesættelser", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Kortfattet CJK (文言)", "description": "Klassisk kinesisk ultra-kortfattet stil (kun tilgængelig for kinesisk)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin- og tilfældige kombinationer skifter til en anden forbindelse ved hver anmodning i stedet for at fastlåse en hel samtale til én forbindelse via den første meddelelses hash. Lad den være slået fra for at bevare prompt-cache-hits ved samtaler med flere ture. Tilsidesættelser pr. kombination har forrang.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Maskering af legitimationsoplysninger", "credentialRedactionDesc": "Masker API-nøgler, tokens og hemmeligheder fra kontekst sendt til udbydere og fra svar.", "enableCredentialRedaction": "Aktivér maskering af legitimationsoplysninger", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Aktiver motoren", "enableDescription": "Kører sidst i stakken (efter RTK/Caveman renser teksten, konverterer OmniGlyph resten til billeder) og kører også selvstændigt via omniglyph-tilstand. Dette er en forhåndsvisning og forbliver deaktiveret som standard, indtil end-to-end-validering er fuldført.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Gemt.", "saveFailed": "Kunne ikke gemme.", "enableAria": "Aktiver OmniGlyph-motoren", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksimum", "grokAutoTopUpMonth": "måned", "grokAdditionalCredits": "Yderligere Credits", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Fuldmagt", "budgetManagement": "Budgetstyring", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Første token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index c1ffc929ea..2b4c363146 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visuelle Anforderungszeitleiste", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "öffnen", "close": "schließen" }, - "noResults": "Keine Ergebnisse", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Keine Ergebnisse" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kontingentanteil", "discovery": "Discovery", "freeProviderRankings": "Rangliste kostenloser Anbieter", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Kostenlose Tarife", "gamification": "Gamification", "leaderboard": "Bestenliste", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Dieser Anbieter ist veraltet", "riskNotice": { "title": "Vor dem Fortfahren", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Anbieter mit Nutzungseinschränkungen — für Details klicken", "oauth": "Dieser Anbieter verwendet Ihre offizielle Produktsitzung/OAuth, die nicht für die Proxy-/Router-Nutzung autorisiert ist. Wir empfehlen keine intensive Nutzung durch autonome Agenten (im OpenCloud-Stil, lange mehrstufige Abläufe, große Batches) — der Upstream-Anbieter kann darauf reagieren, indem er das Konto einschränkt oder sperrt. Nutzung auf eigene Gefahr.", "webCookie": "Dieser Anbieter authentifiziert sich über Ihre Web-Sitzungscookies. Der Upstream-Dienst kann die Sitzung jederzeit ungültig machen, sodass Sie sich erneut anmelden müssen. Nicht empfohlen für lange unbeaufsichtigte Vorgänge. Nutzung auf eigene Gefahr.", @@ -5107,9 +5111,9 @@ "cancel": "Abbrechen" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Deaktiviert", "enableProvider": "Anbieter aktivieren", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Überspringe {count} vorhandene Modelle", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "Automatisches Abrufen von Upstream-Modellen", + "autoFetchModelsTooltip": "Abrufen und Zwischenspeichern von Upstream-Modellen bei Bedarf", + "autoFetchModelsEnabled": "Upstream-Modell Auto-Fetch aktiviert", + "autoFetchModelsDisabled": "Auto-Abholung des Upstream-Modells deaktiviert", + "autoFetchModelsToggleFailed": "Fehler beim Umschalten des automatischen Abrufs des Upstream-Modells", + "autoFetchModelsPartialFailure": "Einige Verbindungen wurden aktualisiert, aber das automatische Abrufen des upstream-Modells wurde nicht überall geändert.", + "overridesUpstreamModel": "Überschreibt upstream", + "overridesUpstreamModelHint": "Ihre Einstellungen überschreiben dieses übergeordnete Modell", + "resetToUpstreamDefaults": "Ursprüngliche Standardeinstellungen wiederherstellen", + "resetToUpstreamDefaultsSuccess": "Ursprüngliche Standardwerte des Upstream-Modells wiederhergestellt", + "resetToUpstreamDefaultsFailed": "Wiederherstellung der Standardwerte des upstream-Modells fehlgeschlagen", "autoSyncTooltip": "Modellliste automatisch alle 24 Stunden aktualisieren (konfigurierbar über MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-Sync aktiviert — Modelle werden regelmäßig aktualisiert", "autoSyncDisabled": "Auto-Sync deaktiviert", @@ -5439,17 +5454,17 @@ "interceptionLoadError": "Fehler beim Laden der Interzeptionseinstellungen: {error}", "interceptionSaveError": "Fehler beim Speichern der Interzeptionseinstellungen: {error}", "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "Bewerben Sie die Modelle dieses Anbieters unter claude/<provider>/<model> Spiegel-IDs, damit das Gateway-Modellentdeckung von Claude Code sie auflisten kann. Standardmäßig deaktiviert — das Aktivieren verdoppelt die Katalogeinträge für alle Kunden.", - "ccAliasProviderLevelLabel": "Anbieter standardmäßig", - "ccAliasModelOverridesLabel": "Pro-Modell-Überschreibungen", - "ccAliasModelOverrideAriaLabel": "Überschreibung für {modelId}", - "ccAliasStateInherit": "Erben", - "ccAliasStateOn": "Ein", - "ccAliasStateOff": "Aus", - "ccAliasAddModelPlaceholder": "Modell-ID (z. B. gpt-4o)", - "ccAliasAddModelButton": "Überschreibung hinzufügen", - "ccAliasLoadError": "Fehler beim Laden der discovery-alias-Einstellungen: {error}", - "ccAliasSaveError": "Fehler beim Speichern der discovery-alias-Einstellung: {error}", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Verbinden Sie Galadriel mit einem API-Schlüssel.", "predibase": "$25 kostenloses Testguthaben (30 Tage Gültigkeit)", "chenzk": "OpenAI-kompatibles Gateway mit einem Live-Modellkatalog unter chenzk.top.", - "freepik": "Generieren Sie Bilder mit der Mystic-API von Freepik.", + "magnific": "Generieren Sie Bilder mit Magnific Mystic.", "freetheai": "Kostenloses OpenAI-kompatibles Gateway mit Passthrough-Modellunterstützung.", "g4f-gemini": "Kostenloser schlüsselloser g4f.space-Reverse-Proxy zu Gemini, begrenzt auf 5 Anfragen pro Minute.", "g4f-groq": "Kostenloser schlüsselloser g4f.space-Reverse-Proxy zu Groq, begrenzt auf 5 Anfragen pro Minute.", @@ -6209,6 +6224,7 @@ "claude": "Claude Code mit dem bestehenden OAuth-Flow verbinden.", "cline": "Cline mit dem bestehenden OAuth-Flow verbinden.", "cursor": "Cursor IDE mit dem bestehenden OAuth-Flow verbinden.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "GitHub Copilot mit dem bestehenden OAuth-Flow verbinden.", "gitlab-duo": "OAuth-Anwendung mit den Scopes ai_features + read_user. Konfigurieren Sie GITLAB_DUO_OAUTH_CLIENT_ID und optional GITLAB_DUO_OAUTH_CLIENT_SECRET auf dieser OmniRoute-Instanz.", "kilocode": "Kilo Code mit dem bestehenden OAuth-Flow verbinden.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "In Abklingzeit", "codexPoolUsed": "verwendet", "codexPoolUntil": "Bis {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonymer Fallback", "anonymousFallbackDesc": "Wenn alle konfigurierten Verbindungen erschöpft sind (Kontingent, Guthaben oder Ablauf), verwenden Sie vorübergehend die schlüssellose Stufe dieses Anbieters. Deaktivieren Sie dies, um diesen Anbieter zu überspringen, anstatt anonyme Anfragen zu senden – empfohlen, wenn die schlüssellose Stufe diese ablehnt (401).", "anonymousFallbackEnabled": "Anonymer Fallback für {provider} aktiviert", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Einstellungen für den gespeicherten Modell-Endpunkt", "searchByModelAria": "Nach Modell suchen", "selectSupportedEndpoint": "Wählen Sie mindestens einen unterstützten Endpunkt aus", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Automatisches Abrufen von Upstream-Modellen", - "autoFetchModelsEnabled": "Upstream-Modell Auto-Fetch aktiviert", - "autoFetchModelsDisabled": "Auto-Abholung des Upstream-Modells deaktiviert", - "autoFetchModelsTooltip": "Abrufen und Zwischenspeichern von Upstream-Modellen bei Bedarf", - "overridesUpstreamModel": "Überschreibt upstream", - "autoFetchModelsToggleFailed": "Fehler beim Umschalten des automatischen Abrufs des Upstream-Modells", - "autoFetchModelsPartialFailure": "Einige Verbindungen wurden aktualisiert, aber das automatische Abrufen des upstream-Modells wurde nicht überall geändert.", - "overridesUpstreamModelHint": "Ihre Einstellungen überschreiben dieses übergeordnete Modell", - "resetToUpstreamDefaultsSuccess": "Ursprüngliche Standardwerte des Upstream-Modells wiederhergestellt", - "resetToUpstreamDefaults": "Ursprüngliche Standardeinstellungen wiederherstellen", - "resetToUpstreamDefaultsFailed": "Wiederherstellung der Standardwerte des upstream-Modells fehlgeschlagen" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Einstellungen", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Gesperrte Keywords", "customBannedSignalsDesc": "Zusätzliche Keywords, die die Erkennung einer dauerhaften Kontosperrung auslösen. Integrierte Keywords gelten immer.", "customBannedSignalsPlaceholder": "z. B. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "konfiguriert", "none": "Keine", "modelOverrideValuePlaceholder": "Numerischer Wert", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Schlüssel-Wert hinzufügen", "noModelOverrides": "Keine Overrides für dieses Modell konfiguriert.", "modelOverrideLoadFailed": "Modell-Overrides konnten nicht geladen werden", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Knappe CJK (文言)", "description": "Klassisch-chinesischer, extrem knapper Stil (nur für Chinesisch verfügbar)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-Robin- und Zufallskombinationen wechseln bei jeder Anfrage zu einer anderen Verbindung, anstatt eine gesamte Konversation über den Hash der ersten Nachricht an eine Verbindung zu binden. Deaktiviert lassen, um Prompt-Cache-Treffer für Multi-Turn-Chats zu erhalten. Überschreibungen pro Kombination haben Vorrang.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Schwärzung von Anmeldedaten", "credentialRedactionDesc": "API-Schlüssel, Token und Geheimnisse aus dem an Anbieter gesendeten Kontext und aus Antworten schwärzen.", "enableCredentialRedaction": "Schwärzung von Anmeldedaten aktivieren", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Engine aktivieren", "enableDescription": "Wird als Letztes im Stack ausgeführt (nachdem RTK/Caveman den Text bereinigt hat, konvertiert OmniGlyph den Rest in Bilder) und läuft auch eigenständig im omniglyph-Modus. Dies ist eine Vorschau und bleibt standardmäßig deaktiviert, bis die End-to-End-Validierung abgeschlossen ist.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Gespeichert.", "saveFailed": "Konnte nicht gespeichert werden.", "enableAria": "OmniGlyph-Engine aktivieren", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "Monat", "grokAdditionalCredits": "Zusätzliche Credits", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Stellvertreter", "budgetManagement": "Budgetverwaltung", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Erster Token", @@ -13217,7 +13250,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Angebote", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13758,36 +13791,36 @@ "trialDays": "{days, plural, one {# Tag} other {# Tage}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index aab9e6eddc..92b0e725b4 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -6214,7 +6214,7 @@ "galadriel": "Connect Galadriel with an API key.", "predibase": "$25 free trial credits (30-day validity)", "chenzk": "OpenAI-compatible gateway with a live model catalog at chenzk.top.", - "freepik": "Generate images with Freepik's Mystic API.", + "magnific": "Generate images with Magnific Mystic.", "freetheai": "Free OpenAI-compatible gateway with passthrough model support.", "g4f-gemini": "Free no-key g4f.space reverse proxy to Gemini, limited to 5 requests per minute.", "g4f-groq": "Free no-key g4f.space reverse proxy to Groq, limited to 5 requests per minute.", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 6b460d7a8f..a6b357e222 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Línea de tiempo de solicitudes visuales", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "abrir", "close": "cerrar" }, - "noResults": "Sin resultados", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Sin resultados" }, "webhooks": { "title": "Ganchos web", @@ -1739,8 +1739,8 @@ "quotaShare": "Quota Share", "discovery": "Discovery", "freeProviderRankings": "Free Provider Rankings", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Free Tiers", "gamification": "Gamification", "leaderboard": "Leaderboard", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Este proveedor ha quedado obsoleto.", "riskNotice": { "title": "Before continuing", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provider with usage caveats — click for details", "oauth": "This provider uses your official product session/OAuth, which is not authorized for proxy/router use. We don't recommend intensive autonomous agent usage (OpenCloud-style, long multi-step flows, large batches) — the upstream may react by restricting or banning the account. Use at your own risk.", "webCookie": "This provider authenticates through your web session cookies. The upstream service may invalidate the session at any time, requiring you to log in again. Not recommended for long unattended operations. Use at your own risk.", @@ -5107,9 +5111,9 @@ "cancel": "Cancel" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Deshabilitado", "enableProvider": "Habilitar proveedor", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Omitiendo {count} modelos existentes", "autoSync": "Sincronización automática", "autoSyncShort": "Sincronizar", + "autoFetchModels": "Obtención automática de modelos upstream", + "autoFetchModelsTooltip": "Obtener y almacenar en caché los modelos de upstream cuando sea necesario", + "autoFetchModelsEnabled": "Modelo de upstream auto-fetch habilitado", + "autoFetchModelsDisabled": "La recuperación automática del modelo upstream está desactivada", + "autoFetchModelsToggleFailed": "Error al alternar la auto-recuperación del modelo upstream", + "autoFetchModelsPartialFailure": "Algunas conexiones se actualizaron, pero el auto-fetch del modelo upstream no se cambió en todas partes", + "overridesUpstreamModel": "Sobrescribe el upstream", + "overridesUpstreamModelHint": "Tus configuraciones anulan este modelo de upstream", + "resetToUpstreamDefaults": "Restaurar valores predeterminados del upstream", + "resetToUpstreamDefaultsSuccess": "Restaurados los valores predeterminados del modelo upstream", + "resetToUpstreamDefaultsFailed": "No se pudo restaurar los valores predeterminados del modelo upstream", "autoSyncTooltip": "Actualiza automáticamente la lista de modelos cada 24 horas (configurable vía MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Sincronización automática activada — los modelos se actualizarán periódicamente", "autoSyncDisabled": "Sincronización automática desactivada", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Rewrite native web_fetch tool calls to OmniRoute's /v1/web/fetch.", "interceptionLoadError": "Failed to load interception settings: {error}", "interceptionSaveError": "Failed to save interception settings: {error}", - "ccAliasSectionTitle": "Exponer en Claude Code (claude/…)", - "ccAliasSectionHint": "Anunciar los modelos de este proveedor bajo claude/<provider>/<model> IDs de espejo para que el descubrimiento de modelos de la puerta de enlace de Claude Code pueda listarlos. Desactivado por defecto; habilitar esto duplica las entradas del catálogo para todos los clientes.", - "ccAliasProviderLevelLabel": "Proveedor predeterminado", - "ccAliasModelOverridesLabel": "Sobrescrituras por modelo", - "ccAliasModelOverrideAriaLabel": "Sobrescribir para {modelId}", - "ccAliasStateInherit": "Heredar", - "ccAliasStateOn": "Encendido", - "ccAliasStateOff": "Apagar", - "ccAliasAddModelPlaceholder": "ID del modelo (p. ej. gpt-4o)", - "ccAliasAddModelButton": "Agregar anulación", - "ccAliasLoadError": "Error al cargar la configuración de discovery-alias: {error}", - "ccAliasSaveError": "Error al guardar la configuración de alias de descubrimiento: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Connect Galadriel with an API key.", "predibase": "$25 free trial credits (30-day validity)", "chenzk": "OpenAI-compatible gateway with a live model catalog at chenzk.top.", - "freepik": "Generate images with Freepik's Mystic API.", + "magnific": "Generate images with Freepik's Mystic API.", "freetheai": "Free OpenAI-compatible gateway with passthrough model support.", "g4f-gemini": "Free no-key g4f.space reverse proxy to Gemini, limited to 5 requests per minute.", "g4f-groq": "Free no-key g4f.space reverse proxy to Groq, limited to 5 requests per minute.", @@ -6209,6 +6224,7 @@ "claude": "Connect Claude Code with the existing OAuth flow.", "cline": "Connect Cline with the existing OAuth flow.", "cursor": "Connect Cursor IDE with the existing OAuth flow.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Connect GitHub Copilot with the existing OAuth flow.", "gitlab-duo": "OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance.", "kilocode": "Connect Kilo Code with the existing OAuth flow.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "En espera", "codexPoolUsed": "usado", "codexPoolUntil": "Hasta {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Recaudación anónima", "anonymousFallbackDesc": "Cuando todas las conexiones configuradas están agotadas (cuota, créditos o expiración), utiliza temporalmente el nivel sin clave de este proveedor. Desactiva para omitir este proveedor en lugar de enviar solicitudes anónimas — recomendado cuando el nivel sin clave las rechaza (401).", "anonymousFallbackEnabled": "Fallback anónimo habilitado para {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Configuración del punto final del modelo guardado", "searchByModelAria": "Buscar por modelo", "selectSupportedEndpoint": "Seleccione al menos un endpoint compatible", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsEnabled": "Modelo de upstream auto-fetch habilitado", - "autoFetchModels": "Obtención automática de modelos upstream", - "autoFetchModelsTooltip": "Obtener y almacenar en caché los modelos de upstream cuando sea necesario", - "autoFetchModelsDisabled": "La recuperación automática del modelo upstream está desactivada", - "autoFetchModelsToggleFailed": "Error al alternar la auto-recuperación del modelo upstream", - "overridesUpstreamModel": "Sobrescribe el upstream", - "autoFetchModelsPartialFailure": "Algunas conexiones se actualizaron, pero el auto-fetch del modelo upstream no se cambió en todas partes", - "overridesUpstreamModelHint": "Tus configuraciones anulan este modelo de upstream", - "resetToUpstreamDefaults": "Restaurar valores predeterminados del upstream", - "resetToUpstreamDefaultsFailed": "No se pudo restaurar los valores predeterminados del modelo upstream", - "resetToUpstreamDefaultsSuccess": "Restaurados los valores predeterminados del modelo upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Configuración", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Banned Keywords", "customBannedSignalsDesc": "Additional keywords that trigger permanent account ban detection. Built-in keywords always apply.", "customBannedSignalsPlaceholder": "e.g. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "configured", "none": "None", "modelOverrideValuePlaceholder": "Numeric value", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Add key value", "noModelOverrides": "No overrides configured for this model.", "modelOverrideLoadFailed": "Failed to load model overrides", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Terse CJK (文言)", "description": "Classical-Chinese ultra-terse style (available only for Chinese)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin and random combos rotate to a different connection on every request instead of pinning a whole conversation to one connection by the first-message hash. Leave off to preserve prompt-cache hits for multi-turn chats. Per-combo overrides take precedence.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Credential Redaction", "credentialRedactionDesc": "Redact API keys, tokens, and secrets from context sent to providers and from responses.", "enableCredentialRedaction": "Enable credential redaction", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Enable the engine", "enableDescription": "Runs last in the stack (after RTK/Caveman cleans the text, OmniGlyph converts the remainder to images) and also runs standalone through omniglyph mode. This is a preview and remains off by default until end-to-end validation is complete.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Saved.", "saveFailed": "Could not save.", "enableAria": "Enable the OmniGlyph engine", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "máx", "grokAutoTopUpMonth": "mes", "grokAdditionalCredits": "Créditos Adicionales", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "registrador", "proxyTab": "apoderado", "budgetManagement": "Gestión Presupuestaria", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "First Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 9962332860..35f12114ef 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "زمان‌بندی درخواست بصری", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "باز کردن", "close": "بستن" }, - "noResults": "هیچ نتیجه‌ای یافت نشد", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "هیچ نتیجه‌ای یافت نشد" }, "webhooks": { "title": "وب هوک ها", @@ -1739,8 +1739,8 @@ "quotaShare": "اشتراک سهمیه", "discovery": "اکتشاف", "freeProviderRankings": "رتبه‌بندی ارائه‌دهندگان رایگان", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "طرح‌های رایگان", "gamification": "بازی‌وارسازی", "leaderboard": "جدول رده‌بندی", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "این ارائه دهنده منسوخ شده است", "riskNotice": { "title": "قبل از ادامه", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "ارائه‌دهنده با هشدارهای استفاده — برای جزئیات کلیک کنید", "oauth": "این ارائه‌دهنده از نشست/OAuth رسمی محصول شما استفاده می‌کند که برای استفاده از پروکسی/روتر مجاز نیست. ما استفاده فشرده از عامل‌های خودکار (به سبک OpenCloud، جریان‌های چندمرحله‌ای طولانی، دسته‌های بزرگ) را توصیه نمی‌کنیم — ممکن است سرویس بالادستی با محدود کردن یا مسدود کردن حساب واکنش نشان دهد. با مسئولیت خودتان استفاده کنید.", "webCookie": "این ارائه‌دهنده از طریق کوکی‌های نشست وب شما احراز هویت می‌کند. سرویس بالادستی ممکن است در هر زمان نشست را باطل کند و شما را ملزم به ورود مجدد نماید. برای عملیات طولانی بدون نظارت توصیه نمی‌شود. با مسئولیت خودتان استفاده کنید.", @@ -5107,9 +5111,9 @@ "cancel": "لغو" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "مدل‌های بالادستی را به‌طور خودکار دریافت کنید", + "autoFetchModelsTooltip": "مدل‌های بالادستی را در صورت نیاز دریافت و کش کنید", + "autoFetchModelsEnabled": "مدل بالادستی بارگذاری خودکار فعال است", + "autoFetchModelsDisabled": "مدل upstream بارگذاری خودکار غیرفعال است", + "autoFetchModelsToggleFailed": "عدم موفقیت در تغییر حالت بارگیری خودکار مدل upstream", + "autoFetchModelsPartialFailure": "برخی اتصالات به‌روزرسانی شدند، اما مدل بالادستی auto-fetch در همه جا تغییر نکرده است", + "overridesUpstreamModel": "بازنویسی upstream", + "overridesUpstreamModelHint": "تنظیمات شما این مدل بالادستی را نادیده می‌گیرند", + "resetToUpstreamDefaults": "بازگرداندن تنظیمات پیش‌فرض upstream", + "resetToUpstreamDefaultsSuccess": "تنظیمات پیش‌فرض مدل بالادستی بازیابی شد", + "resetToUpstreamDefaultsFailed": "بازگردانی پیش‌فرض‌های مدل upstream ناموفق بود", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "بازنویسی فراخوانی‌های ابزار بومی web_fetch به /v1/web/fetch در OmniRoute.", "interceptionLoadError": "بارگیری تنظیمات رهگیری ناموفق بود: {error}", "interceptionSaveError": "ذخیره تنظیمات رهگیری ناموفق بود: {error}", - "ccAliasSectionTitle": "در کد کلاود (claude/…) نمایان کنید", - "ccAliasSectionHint": "مدل‌های این ارائه‌دهنده را تحت شناسه‌های آینه claude/<provider>/<model> تبلیغ کنید تا کشف مدل‌های دروازه کد کلود آن‌ها را فهرست کند. به‌طور پیش‌فرض غیرفعال است — فعال‌سازی این گزینه تعداد ورودی‌های کاتالوگ را برای تمام مشتریان دو برابر می‌کند.", - "ccAliasProviderLevelLabel": "ارائه‌دهنده پیش‌فرض", - "ccAliasModelOverridesLabel": "بازنویسی‌های هر مدل", - "ccAliasModelOverrideAriaLabel": "بازنویسی برای {modelId}", - "ccAliasStateInherit": "به ارث بردن", - "ccAliasStateOn": "روشن", - "ccAliasStateOff": "خاموش", - "ccAliasAddModelPlaceholder": "شناسه مدل (به عنوان مثال gpt-4o)", - "ccAliasAddModelButton": "اضافه کردن نادیده‌گیری", - "ccAliasLoadError": "بارگذاری تنظیمات discovery-alias با شکست مواجه شد: {error}", - "ccAliasSaveError": "ذخیره تنظیمات discovery-alias با شکست مواجه شد: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "اتصال به Galadriel با یک کلید API.", "predibase": "$25 اعتبار آزمایشی رایگان (اعتبار ۳۰ روزه)", "chenzk": "درگاه سازگار با OpenAI با یک کاتالوگ مدل زنده در chenzk.top.", - "freepik": "تولید تصاویر با Mystic API مربوط به Freepik.", + "magnific": "تولید تصاویر با Mystic API مربوط به Freepik.", "freetheai": "درگاه رایگان سازگار با OpenAI با پشتیبانی از مدل passthrough.", "g4f-gemini": "پروکسی معکوس رایگان و بدون کلید g4f.space به Gemini، محدود به 5 درخواست در دقیقه.", "g4f-groq": "پروکسی معکوس رایگان و بدون کلید g4f.space به Groq، محدود به 5 درخواست در دقیقه.", @@ -6209,6 +6224,7 @@ "claude": "اتصال به Claude Code با جریان OAuth موجود.", "cline": "اتصال به Cline با جریان OAuth موجود.", "cursor": "اتصال به Cursor IDE با جریان OAuth موجود.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "اتصال به GitHub Copilot با جریان OAuth موجود.", "gitlab-duo": "برنامه OAuth با اسکوپ‌های ai_features + read_user. متغیرهای GITLAB_DUO_OAUTH_CLIENT_ID و به صورت اختیاری GITLAB_DUO_OAUTH_CLIENT_SECRET را روی این نمونه OmniRoute پیکربندی کنید.", "kilocode": "اتصال به Kilo Code با جریان OAuth موجود.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "در دوره انتظار", "codexPoolUsed": "مصرف‌شده", "codexPoolUntil": "تا {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "پشتیبانی ناشناس", "anonymousFallbackDesc": "زمانی که تمام اتصالات پیکربندی‌شده تمام شده‌اند (سهمیه، اعتبار یا انقضا)، به‌طور موقت از سطح بدون کلید این ارائه‌دهنده استفاده کنید. برای رد کردن این ارائه‌دهنده به‌جای ارسال درخواست‌های ناشناس خاموش کنید — این کار زمانی توصیه می‌شود که سطح بدون کلید آن‌ها را رد کند (401).", "anonymousFallbackEnabled": "پشتیبانی ناشناس برای {provider} فعال شد", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "تنظیمات نقطه پایانی مدل ذخیره شده", "searchByModelAria": "جستجو بر اساس مدل", "selectSupportedEndpoint": "حداقل یک نقطه پایانی پشتیبانی شده را انتخاب کنید", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "مدل‌های بالادستی را به‌طور خودکار دریافت کنید", - "autoFetchModelsTooltip": "مدل‌های بالادستی را در صورت نیاز دریافت و کش کنید", - "autoFetchModelsDisabled": "مدل upstream بارگذاری خودکار غیرفعال است", - "autoFetchModelsEnabled": "مدل بالادستی بارگذاری خودکار فعال است", - "autoFetchModelsToggleFailed": "عدم موفقیت در تغییر حالت بارگیری خودکار مدل upstream", - "overridesUpstreamModel": "بازنویسی upstream", - "autoFetchModelsPartialFailure": "برخی اتصالات به‌روزرسانی شدند، اما مدل بالادستی auto-fetch در همه جا تغییر نکرده است", - "resetToUpstreamDefaults": "بازگرداندن تنظیمات پیش‌فرض upstream", - "overridesUpstreamModelHint": "تنظیمات شما این مدل بالادستی را نادیده می‌گیرند", - "resetToUpstreamDefaultsSuccess": "تنظیمات پیش‌فرض مدل بالادستی بازیابی شد", - "resetToUpstreamDefaultsFailed": "بازگردانی پیش‌فرض‌های مدل upstream ناموفق بود" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "کلمات کلیدی ممنوع", "customBannedSignalsDesc": "کلمات کلیدی اضافی که باعث تشخیص مسدودسازی دائمی حساب می‌شوند. کلمات کلیدی داخلی همیشه اعمال می‌شوند.", "customBannedSignalsPlaceholder": "مثلاً api key revoked", @@ -7210,6 +7203,7 @@ "configured": "پیکربندی‌شده", "none": "هیچ‌کدام", "modelOverrideValuePlaceholder": "مقدار عددی", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "افزودن کلید-مقدار", "noModelOverrides": "هیچ بازنویسی‌ای برای این مدل پیکربندی نشده است.", "modelOverrideLoadFailed": "بارگذاری بازنویسی‌های مدل ناموفق بود", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK موجز (文言)", "description": "سبک فوق‌موجز چینی کلاسیک (فقط برای زبان چینی در دسترس است)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "ترکیب‌های نوبت‌گردشی و تصادفی در هر درخواست به یک اتصال متفاوت منتقل می‌شوند، به جای اینکه کل گفتگو را بر اساس هش اولین پیام به یک اتصال پین کنند. برای حفظ هیت‌های prompt-cache در چت‌های چند نوبته، این گزینه را غیرفعال بگذارید. اولویت با بازنویسی‌های اختصاصی هر ترکیب است.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "سانسور اطلاعات اعتبارنامه‌ای", "credentialRedactionDesc": "سانسور کردن کلیدهای API، توکن‌ها و اسرار از بافت ارسال شده به ارائه‌دهندگان و از پاسخ‌ها.", "enableCredentialRedaction": "فعال‌سازی سانسور اطلاعات اعتبارنامه‌ای", @@ -8621,6 +8623,27 @@ }, "enableTitle": "فعال‌سازی موتور", "enableDescription": "در انتهای پشته اجرا می‌شود (پس از اینکه RTK/Caveman متن را پاکسازی کرد، OmniGlyph باقی‌مانده را به تصویر تبدیل می‌کند) و همچنین به‌صورت مستقل از طریق حالت omniglyph اجرا می‌شود. این یک پیش‌نمایش است و تا زمان تکمیل اعتبارسنجی سرتاسری به‌طور پیش‌فرض خاموش می‌ماند.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "ذخیره شد.", "saveFailed": "ذخیره نشد.", "enableAria": "فعال‌سازی موتور OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "حداکثر", "grokAutoTopUpMonth": "ماه", "grokAdditionalCredits": "اعتبارات اضافی", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "اولین توکن", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 6b882ce670..fd15e9dfaa 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visuaalinen pyyntöaikajana", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "avaa", "close": "sulje" }, - "noResults": "Ei tuloksia", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Ei tuloksia" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kiintiöosuus", "discovery": "Löytäminen", "freeProviderRankings": "Ilmaisten tarjoajien sijoitukset", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Ilmaiset tasot", "gamification": "Pelillistäminen", "leaderboard": "Tulostaulukko", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Tämä palveluntarjoaja on poistettu käytöstä", "riskNotice": { "title": "Ennen jatkamista", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Tarjoaja, jolla on käyttöä koskevia huomautuksia — napsauta nähdäksesi lisätiedot", "oauth": "Tämä tarjoaja käyttää virallista tuote-istuntoasi/OAuthia, jota ei ole valtuutettu välityspalvelin-/reititinkäyttöön. Emme suosittele intensiivistä autonomisten agenttien käyttöä (OpenCloud-tyyliset, pitkät monivaiheiset työnkulut, suuret erät) — ylävirta saattaa reagoida rajoittamalla tiliä tai estämällä sen. Käyttö omalla vastuulla.", "webCookie": "Tämä tarjoaja todennetaan verkkosessiosi evästeiden kautta. Ylävirran palvelu voi mitätöidä istunnon milloin tahansa, jolloin sinun on kirjauduttava uudelleen sisään. Ei suositella pitkiin valvomattomiin toimintoihin. Käyttö omalla vastuulla.", @@ -5107,9 +5111,9 @@ "cancel": "Peruuta" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Ei käytössä", "enableProvider": "Ota palveluntarjoaja käyttöön", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Ohitetaan {count} olemassa olevaa mallia", "autoSync": "Automaattinen synkronointi", "autoSyncShort": "Synkronointi", + "autoFetchModels": "Hae automaattisesti upstream-malleja", + "autoFetchModelsTooltip": "Hae ja vältä ylösvirtaisten mallien välimuisti tarvittaessa", + "autoFetchModelsEnabled": "Ylävirran mallin automaattinen haku käytössä", + "autoFetchModelsDisabled": "Ylöspäin suuntautuvan mallin automaattinen haku pois käytöstä", + "autoFetchModelsToggleFailed": "Epäonnistui ylösvirran mallin automaattihaku kytkemisessä", + "autoFetchModelsPartialFailure": "Joitakin yhteyksiä päivitettiin, mutta ylävirran mallin automaattista hakua ei muutettu kaikkialla", + "overridesUpstreamModel": "Ylikirjoittaa ylävirran", + "overridesUpstreamModelHint": "Asetuksesi ohittavat tämän ylävirran mallin", + "resetToUpstreamDefaults": "Palauta upstream-oletukset", + "resetToUpstreamDefaultsSuccess": "Palautettiin ylävirran mallin oletukset", + "resetToUpstreamDefaultsFailed": "Palautus upstream-mallin oletusasetuksista epäonnistui", "autoSyncTooltip": "Päivitä malliluettelo automaattisesti 24 tunnin välein (konfiguroitavissa kohdassa MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automaattinen synkronointi käytössä – mallit päivittyvät säännöllisesti", "autoSyncDisabled": "Automaattinen synkronointi poistettu käytöstä", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Uudelleenkirjoita natiivit web_fetch-työkalukutsut OmniRouten osoitteeseen /v1/web/fetch.", "interceptionLoadError": "Sieppausasetusten lataaminen epäonnistui: {error}", "interceptionSaveError": "Sieppausasetusten tallentaminen epäonnistui: {error}", - "ccAliasSectionTitle": "Avaa Claude-koodissa (claude/…)", - "ccAliasSectionHint": "Mainosta tämän tarjoajan malleja claude/<provider>/<model> peilidien alla, jotta Claude Coden porttimallin löytö voi listata ne. Oletusarvoisesti pois päältä — tämän aktivointi kaksinkertaistaa luettelon merkinnät kaikille asiakkaille.", - "ccAliasProviderLevelLabel": "Palveluntarjoajan oletus", - "ccAliasModelOverridesLabel": "Per-mallin ylitykset", - "ccAliasModelOverrideAriaLabel": "Ylikirjoitus {modelId} varten", - "ccAliasStateInherit": "Peri", - "ccAliasStateOn": "Päällä", - "ccAliasStateOff": "Pois", - "ccAliasAddModelPlaceholder": "Mallin tunnus (esim. gpt-4o)", - "ccAliasAddModelButton": "Lisää ohitus", - "ccAliasLoadError": "Epäonnistui lataamaan discovery-alias-asetuksia: {error}", - "ccAliasSaveError": "Asetuksen discovery-alias tallentaminen epäonnistui: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Yhdistä Galadriel API-avaimella.", "predibase": "$25 ilmaista kokeilusaldoa (voimassa 30 päivää)", "chenzk": "OpenAI-yhteensopiva yhdyskäytävä reaaliaikaisella malliluettelolla osoitteessa chenzk.top.", - "freepik": "Luo kuvia Freepikin Mystic API:lla.", + "magnific": "Luo kuvia Freepikin Mystic API:lla.", "freetheai": "Ilmainen OpenAI-yhteensopiva yhdyskäytävä läpivientimallien tuella.", "g4f-gemini": "Ilmainen avaimeton g4f.space-käänteisvälityspalvelin Geminiin, rajoitettu 5 pyyntöön minuutissa.", "g4f-groq": "Ilmainen avaimeton g4f.space-käänteisvälityspalvelin Groqiin, rajoitettu 5 pyyntöön minuutissa.", @@ -6209,6 +6224,7 @@ "claude": "Yhdistä Claude Code olemassa olevalla OAuth-työnkululla.", "cline": "Yhdistä Cline olemassa olevalla OAuth-työnkululla.", "cursor": "Yhdistä Cursor IDE olemassa olevalla OAuth-työnkululla.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Yhdistä GitHub Copilot olemassa olevalla OAuth-työnkululla.", "gitlab-duo": "OAuth-sovellus ai_features + read_user -käyttöoikeuksilla. Määritä GITLAB_DUO_OAUTH_CLIENT_ID ja valinnaisesti GITLAB_DUO_OAUTH_CLIENT_SECRET tälle OmniRoute-instanssille.", "kilocode": "Yhdistä Kilo Code olemassa olevalla OAuth-työnkululla.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Jäähdytysjaksolla", "codexPoolUsed": "käytetty", "codexPoolUntil": "{value} asti", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonyymi varajärjestelmä", "anonymousFallbackDesc": "Kun kaikki määritetyt yhteydet on käytetty loppuun (kiintiö, krediitit tai vanhentuminen), käytä väliaikaisesti tämän tarjoajan avaimettomaa tasoa. Poista käytöstä tämän tarjoajan ohittamiseksi sen sijaan, että lähetät nimettömiä pyyntöjä — suositellaan, kun avaimeton taso hylkää ne (401).", "anonymousFallbackEnabled": "Anonyymi varajärjestelmä käytössä {provider} varten", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Tallennetun mallin päätepisteen asetukset", "searchByModelAria": "Hae mallin mukaan", "selectSupportedEndpoint": "Valitse vähintään yksi tuettu päätepiste", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Hae automaattisesti upstream-malleja", - "autoFetchModelsEnabled": "Ylävirran mallin automaattinen haku käytössä", - "autoFetchModelsTooltip": "Hae ja vältä ylösvirtaisten mallien välimuisti tarvittaessa", - "autoFetchModelsDisabled": "Ylöspäin suuntautuvan mallin automaattinen haku pois käytöstä", - "overridesUpstreamModel": "Ylikirjoittaa ylävirran", - "autoFetchModelsToggleFailed": "Epäonnistui ylösvirran mallin automaattihaku kytkemisessä", - "autoFetchModelsPartialFailure": "Joitakin yhteyksiä päivitettiin, mutta ylävirran mallin automaattista hakua ei muutettu kaikkialla", - "resetToUpstreamDefaults": "Palauta upstream-oletukset", - "resetToUpstreamDefaultsSuccess": "Palautettiin ylävirran mallin oletukset", - "resetToUpstreamDefaultsFailed": "Palautus upstream-mallin oletusasetuksista epäonnistui", - "overridesUpstreamModelHint": "Asetuksesi ohittavat tämän ylävirran mallin" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Asetukset", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Kielletyt avainsanat", "customBannedSignalsDesc": "Muut avainsanat, jotka käynnistävät tilin pysyvän eston tunnistuksen. Sisäänrakennetut avainsanat ovat aina käytössä.", "customBannedSignalsPlaceholder": "esim. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "määritetty", "none": "Ei mitään", "modelOverrideValuePlaceholder": "Numeerinen arvo", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Lisää avain-arvo", "noModelOverrides": "Tälle mallille ei ole määritetty ohituksia.", "modelOverrideLoadFailed": "Mallin ohitusten lataaminen epäonnistui", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Tiivis CJK (文言)", "description": "Klassisen kiinan ultra-tiivis tyyli (saatavilla vain kiinaksi)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin- ja satunnaisyhdistelmät vaihtavat eri yhteyteen jokaisella pyynnöllä sen sijaan, että koko keskustelu kiinnitettäisiin yhteen yhteyteen ensimmäisen viestin tiivisteen (hash) perusteella. Jätä pois käytöstä säilyttääksesi prompt-välimuistin osumat monivaiheisissa keskusteluissa. Yhdistelmäkohtaiset ohitukset ovat etusijalla.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Tunnistetietojen peittäminen", "credentialRedactionDesc": "Peitä API-avaimet, tokenit ja salaisuudet tarjoajille lähetettävästä kontekstista sekä vastauksista.", "enableCredentialRedaction": "Ota tunnistetietojen peittäminen käyttöön", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Ota moottori käyttöön", "enableDescription": "Suoritetaan pinossa viimeisenä (sen jälkeen kun RTK/Caveman puhdistaa tekstin ja OmniGlyph muuntaa loput kuviksi) ja suoritetaan myös itsenäisesti omniglyph-tilan kautta. Tämä on esikatseluversio ja pysyy oletusarvoisesti poissa käytöstä, kunnes päästä päähän -validointi on valmis.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Tallennettu.", "saveFailed": "Tallennus epäonnistui.", "enableAria": "Ota OmniGlyph-moottori käyttöön", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "kuukausi", "grokAdditionalCredits": "Lisäluotit", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Kirjaaja", "proxyTab": "Välityspalvelin", "budgetManagement": "Budjetin hallinta", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Ensimmäinen tokeni", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 84ffa972ec..64fc699674 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Chronologie visuelle des requêtes", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1282,8 +1284,6 @@ "resilienceConnectionsSubtitle": "Cooldown, disjoncteur, état de blocage", "settingsModalityBridge": "Pont de Modalité", "settingsModalityBridgeSubtitle": "Fallback image/audio → texte pour les modèles uniquement textuels", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations", "commandPalette": { "title": "Palette de commandes", "searchPlaceholder": "Rechercher dans les pages, paramètres et outils…", @@ -1739,8 +1739,8 @@ "quotaShare": "Partage de quota", "discovery": "Découverte", "freeProviderRankings": "Classement des fournisseurs gratuits", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Niveaux gratuits", "gamification": "Gamification", "leaderboard": "Classement", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Ce fournisseur est obsolète", "riskNotice": { "title": "Avant de continuer", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Fournisseur avec des restrictions d'utilisation — cliquez pour plus de détails", "oauth": "Ce fournisseur utilise votre session produit officielle/OAuth, ce qui n'est pas autorisé pour une utilisation via proxy/routeur. Nous ne recommandons pas une utilisation intensive par des agents autonomes (style OpenCloud, flux longs à étapes multiples, lots volumineux) — le service amont pourrait réagir en restreignant ou en bannissant le compte. À utiliser à vos risques et périls.", "webCookie": "Ce fournisseur s'authentifie via les cookies de votre session web. Le service amont peut invalider la session à tout moment, vous obligeant à vous reconnecter. Non recommandé pour les opérations longues sans surveillance. À utiliser à vos risques et périls.", @@ -5107,9 +5111,9 @@ "cancel": "Annuler" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Désactivé", "enableProvider": "Activer le fournisseur", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Ignorance de {count} modèles existants", "autoSync": "Synchronisation automatique", "autoSyncShort": "Synchroniser", + "autoFetchModels": "Récupérer automatiquement les modèles en amont", + "autoFetchModelsTooltip": "Récupérer et mettre en cache les modèles en amont si nécessaire", + "autoFetchModelsEnabled": "Récupération automatique du modèle en amont activée", + "autoFetchModelsDisabled": "Récupération automatique du modèle en amont désactivée", + "autoFetchModelsToggleFailed": "Échec de l'activation de la récupération automatique du modèle en amont", + "autoFetchModelsPartialFailure": "Certaines connexions ont été mises à jour, mais l'auto-récupération du modèle en amont n'a pas été modifiée partout", + "overridesUpstreamModel": "Remplace les modifications en amont", + "overridesUpstreamModelHint": "Vos paramètres remplacent ce modèle en amont", + "resetToUpstreamDefaults": "Restaurer les valeurs par défaut en amont", + "resetToUpstreamDefaultsSuccess": "Modèles par défaut de l'amont restaurés", + "resetToUpstreamDefaultsFailed": "Échec de la restauration des paramètres par défaut du modèle en amont", "autoSyncTooltip": "Actualise automatiquement la liste des modèles toutes les 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Synchronisation automatique activée — les modèles seront actualisés périodiquement", "autoSyncDisabled": "Synchronisation automatique désactivée", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Réécrire les appels d'outils natifs web_fetch vers le point de terminaison /v1/web/fetch d'OmniRoute.", "interceptionLoadError": "Échec du chargement des paramètres d'interception : {error}", "interceptionSaveError": "Échec de l'enregistrement des paramètres d'interception : {error}", - "ccAliasSectionTitle": "Exposer dans Claude Code (claude/…)", - "ccAliasSectionHint": "Afficher les modèles de ce fournisseur sous forme d'identifiants miroir claude/<provider>/<model> afin que la découverte de modèles de la passerelle Claude Code puisse les répertorier. Cette option est désactivée par défaut ; son activation double les entrées du catalogue pour tous les clients.", - "ccAliasProviderLevelLabel": "Valeur par défaut du fournisseur", - "ccAliasModelOverridesLabel": "Surcharges par modèle", - "ccAliasModelOverrideAriaLabel": "Surcharge pour {modelId}", - "ccAliasStateInherit": "Hériter", - "ccAliasStateOn": "Activé", - "ccAliasStateOff": "Désactivé", - "ccAliasAddModelPlaceholder": "ID du modèle (par ex. gpt-4o)", - "ccAliasAddModelButton": "Ajouter une surcharge", - "ccAliasLoadError": "Échec du chargement des paramètres d'alias de découverte : {error}", - "ccAliasSaveError": "Échec de l'enregistrement du paramètre d'alias de découverte : {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Connecter Galadriel avec une clé API.", "predibase": "25 $ de crédits d'essai gratuit (validité de 30 jours)", "chenzk": "Passerelle compatible OpenAI avec un catalogue de modèles en direct sur chenzk.top.", - "freepik": "Générer des images avec l'API Mystic de Freepik.", + "magnific": "Générer des images avec l'API Mystic de Freepik.", "freetheai": "Passerelle gratuite compatible OpenAI avec prise en charge des modèles en passthrough.", "g4f-gemini": "Reverse proxy g4f.space gratuit sans clé vers Gemini, limité à 5 requêtes par minute.", "g4f-groq": "Reverse proxy g4f.space gratuit sans clé vers Groq, limité à 5 requêtes par minute.", @@ -6209,6 +6224,7 @@ "claude": "Connecter Claude Code avec le flux OAuth existant.", "cline": "Connecter Cline avec le flux OAuth existant.", "cursor": "Connecter Cursor IDE avec le flux OAuth existant.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Connecter GitHub Copilot avec le flux OAuth existant.", "gitlab-duo": "Application OAuth avec les portées ai_features + read_user. Configurez GITLAB_DUO_OAUTH_CLIENT_ID et éventuellement GITLAB_DUO_OAUTH_CLIENT_SECRET sur cette instance OmniRoute.", "kilocode": "Connecter Kilo Code avec le flux OAuth existant.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "En période d'attente", "codexPoolUsed": "utilisé", "codexPoolUntil": "Jusqu'à {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback anonyme", "anonymousFallbackDesc": "Lorsque toutes les connexions configurées sont épuisées (quota, crédits ou expiration), utilisez temporairement le niveau sans clé de ce fournisseur. Désactivez cette option pour ignorer ce fournisseur au lieu d'envoyer des requêtes anonymes — recommandé lorsque le niveau sans clé les rejette (401).", "anonymousFallbackEnabled": "Fallback anonyme activé pour {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Saved modèles endpoint paramètres", "searchByModelAria": "Rechercher un modèle", "selectSupportedEndpoint": "Sélectionnez au moins un endpoint pris en charge", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Récupérer automatiquement les modèles en amont", - "autoFetchModelsEnabled": "Récupération automatique du modèle en amont activée", - "autoFetchModelsDisabled": "Récupération automatique du modèle en amont désactivée", - "autoFetchModelsTooltip": "Récupérer et mettre en cache les modèles en amont si nécessaire", - "autoFetchModelsToggleFailed": "Échec de l'activation de la récupération automatique du modèle en amont", - "overridesUpstreamModel": "Remplace les modifications en amont", - "overridesUpstreamModelHint": "Vos paramètres remplacent ce modèle en amont", - "resetToUpstreamDefaults": "Restaurer les valeurs par défaut en amont", - "resetToUpstreamDefaultsSuccess": "Modèles par défaut de l'amont restaurés", - "autoFetchModelsPartialFailure": "Certaines connexions ont été mises à jour, mais l'auto-récupération du modèle en amont n'a pas été modifiée partout", - "resetToUpstreamDefaultsFailed": "Échec de la restauration des paramètres par défaut du modèle en amont" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Paramètres", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Mots-clés bannis", "customBannedSignalsDesc": "Mots-clés supplémentaires qui déclenchent la détection de bannissement permanent du compte. Les mots-clés intégrés s'appliquent toujours.", "customBannedSignalsPlaceholder": "ex. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "configuré", "none": "Aucun", "modelOverrideValuePlaceholder": "Valeur numérique", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Ajouter une clé-valeur", "noModelOverrides": "Aucune surcharge configurée pour ce modèle.", "modelOverrideLoadFailed": "Échec du chargement des surcharges de modèle", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK concis (文言)", "description": "Style ultra-concis en chinois classique (disponible uniquement pour le chinois)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Les combinaisons round-robin et aléatoires basculent vers une connexion différente à chaque requête au lieu d'associer toute une conversation à une seule connexion via le hachage du premier message. Laissez désactivé pour préserver les correspondances du cache de prompts pour les discussions multi-tours. Les remplacements par combinaison sont prioritaires.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Masquage des identifiants", "credentialRedactionDesc": "Masquer les clés d'API, les jetons et les secrets du contexte envoyé aux fournisseurs et des réponses.", "enableCredentialRedaction": "Activer le masquage des identifiants", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Activer le moteur", "enableDescription": "S'exécute en dernier dans la pile (après que RTK/Caveman a nettoyé le texte, OmniGlyph convertit le reste en images) et s'exécute également de manière autonome via le mode omniglyph. Il s'agit d'une version préliminaire qui reste désactivée par défaut jusqu'à ce que la validation de bout en bout soit terminée.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Enregistré.", "saveFailed": "Impossible d'enregistrer.", "enableAria": "Activer le moteur OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "mois", "grokAdditionalCredits": "Crédits supplémentaires", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Enregistreur", "proxyTab": "Procuration", "budgetManagement": "Gestion budgétaire", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Premier token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index eb23ac5ebd..5e21f097c1 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "વિઝ્યુઅલ વિનંતી સમયરેખા", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "ખોલો", "close": "બંધ કરો" }, - "noResults": "કોઈ પરિણામો નથી", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "કોઈ પરિણામો નથી" }, "webhooks": { "title": "વેબહુક્સ", @@ -1739,8 +1739,8 @@ "quotaShare": "ક્વોટા શેર", "discovery": "ડિસ્કવરી", "freeProviderRankings": "ફ્રી પ્રોવાઇડર રેન્કિંગ્સ", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "ફ્રી ટાયર્સ", "gamification": "ગેમિફિકેશન", "leaderboard": "લીડરબોર્ડ", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "આ પ્રદાતા નાપસંદ કરવામાં આવી છે", "riskNotice": { "title": "આગળ વધતા પહેલા", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "વપરાશની ચેતવણીઓ સાથેનો પ્રદાતા — વિગતો માટે ક્લિક કરો", "oauth": "આ પ્રદાતા તમારા સત્તાવાર પ્રોડક્ટ સત્ર/OAuth નો ઉપયોગ કરે છે, જે પ્રોક્સી/રાઉટર ઉપયોગ માટે અધિકૃત નથી. અમે સઘન સ્વાયત્ત એજન્ટ વપરાશ (OpenCloud-શૈલી, લાંબા બહુ-પગલાંના પ્રવાહો, મોટા બેચ) ની ભલામણ કરતા નથી — અપસ્ટ્રીમ એકાઉન્ટને પ્રતિબંધિત અથવા બૅન કરીને પ્રતિક્રિયા આપી શકે છે. તમારા પોતાના જોખમે ઉપયોગ કરો.", "webCookie": "આ પ્રદાતા તમારા વેબ સત્ર કૂકીઝ દ્વારા પ્રમાણિત કરે છે. અપસ્ટ્રીમ સેવા કોઈપણ સમયે સત્રને અમાન્ય કરી શકે છે, જેના કારણે તમારે ફરીથી લૉગ ઇન કરવું પડશે. લાંબા અડચણ વગરના ઓપરેશન્સ માટે ભલામણ કરેલ નથી. તમારા પોતાના જોખમે ઉપયોગ કરો.", @@ -5107,9 +5111,9 @@ "cancel": "રદ કરો" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "આપોઆપ અપસ્ટ્રીમ મોડલ્સ લાવો", + "autoFetchModelsTooltip": "જરૂર પડ્યે અપસ્ટ્રીમ મોડલ્સને લાવવા અને કેશ કરવા", + "autoFetchModelsEnabled": "અપસ્ટ્રીમ મોડલ આપોઆપ મેળવવું સક્રિય છે", + "autoFetchModelsDisabled": "અપસ્ટ્રીમ મોડલ આપોઆપ મેળવનાર બંધ છે", + "autoFetchModelsToggleFailed": "અપસ્ટ્રીમ મોડલ ઓટો-ફેચ ટોગલ કરવામાં નિષ્ફળ થયું", + "autoFetchModelsPartialFailure": "કેટલાક કનેક્શન અપડેટ થયા, પરંતુ ઉપરવાળા મોડેલનું ઓટો-ફેચ દરેક જગ્યાએ બદલાયું નથી", + "overridesUpstreamModel": "અપસ્ટ્રીમને ઓવરરાઈડ કરે છે", + "overridesUpstreamModelHint": "તમારા સેટિંગ્સ આ અપસ્ટ્રીમ મોડેલને ઓવરરાઈડ કરે છે", + "resetToUpstreamDefaults": "અપસ્ટ્રીમ ડિફોલ્ટ્સ પુનઃસ્થાપિત કરો", + "resetToUpstreamDefaultsSuccess": "ઉપરવાળી મોડલ ડિફોલ્ટ્સ પુનઃસ્થાપિત કરવામાં આવ્યા", + "resetToUpstreamDefaultsFailed": "અપસ્ટ્રીમ મોડલ ડિફોલ્ટ્સ પુનઃસ્થાપિત કરવામાં નિષ્ફળ થયું", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "મૂળ web_fetch ટૂલ કૉલ્સને OmniRoute ના /v1/web/fetch પર ફરીથી લખો.", "interceptionLoadError": "ઇન્ટરસેપ્શન સેટિંગ્સ લોડ કરવામાં નિષ્ફળ: {error}", "interceptionSaveError": "ઇન્ટરસેપ્શન સેટિંગ્સ સાચવવામાં નિષ્ફળ: {error}", - "ccAliasSectionTitle": "Claude કોડમાં પ્રદર્શિત કરો (claude/…)", - "ccAliasSectionHint": "આ પ્રદાતા ના મોડલ્સને claude/<provider>/<model> મિરર આઈડીઓ હેઠળ જાહેરાત આપો જેથી Claude Code ના ગેટવે મોડલ શોધી શકે. ડિફોલ્ટ દ્વારા બંધ - આને સક્રિય કરવાથી તમામ ક્લાયન્ટો માટે કેટલોગ એન્ટ્રીઓ ડબલ થાય છે.", - "ccAliasProviderLevelLabel": "પ્રદાતા ડિફોલ્ટ", - "ccAliasModelOverridesLabel": "પ્રતિ-મોડલ ઓવરરાઇડ્સ", - "ccAliasModelOverrideAriaLabel": "{modelId} માટે ઓવરરાઈડ", - "ccAliasStateInherit": "વંશજ", - "ccAliasStateOn": "પર", - "ccAliasStateOff": "બંધ", - "ccAliasAddModelPlaceholder": "મોડલ આઈડી (ઉદાહરણ તરીકે gpt-4o)", - "ccAliasAddModelButton": "ઓવરરાઈડ ઉમેરો", - "ccAliasLoadError": "ડિસ્કવરી-એલિયસ સેટિંગ્સ લોડ કરવામાં નિષ્ફળ: {error}", - "ccAliasSaveError": "ડિસ્કવરી-એલિયસ સેટિંગ સાચવવામાં નિષ્ફળ: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "API કી વડે Galadriel ને કનેક્ટ કરો.", "predibase": "$25 મફત ટ્રાયલ ક્રેડિટ્સ (30-દિવસની માન્યતા)", "chenzk": "chenzk.top પર લાઇવ મોડલ કેટલોગ સાથે OpenAI-સુસંગત ગેટવે.", - "freepik": "Freepik ના Mystic API વડે છબીઓ જનરેટ કરો.", + "magnific": "Freepik ના Mystic API વડે છબીઓ જનરેટ કરો.", "freetheai": "પાસથ્રુ મોડલ સપોર્ટ સાથે મફત OpenAI-સુસંગત ગેટવે.", "g4f-gemini": "Gemini માટે મફત નો-કી g4f.space રિવર્સ પ્રોક્સી, પ્રતિ મિનિટ 5 વિનંતીઓ સુધી મર્યાદિત.", "g4f-groq": "Groq માટે મફત નો-કી g4f.space રિવર્સ પ્રોક્સી, પ્રતિ મિનિટ 5 વિનંતીઓ સુધી મર્યાદિત.", @@ -6209,6 +6224,7 @@ "claude": "હાલના OAuth ફ્લો વડે Claude Code ને કનેક્ટ કરો.", "cline": "હાલના OAuth ફ્લો વડે Cline ને કનેક્ટ કરો.", "cursor": "હાલના OAuth ફ્લો વડે Cursor IDE ને કનેક્ટ કરો.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "હાલના OAuth ફ્લો વડે GitHub Copilot ને કનેક્ટ કરો.", "gitlab-duo": "ai_features + read_user સ્કોપ્સ સાથેની OAuth એપ્લિકેશન. આ OmniRoute ઇન્સ્ટન્સ પર GITLAB_DUO_OAUTH_CLIENT_ID અને વૈકલ્પિક રીતે GITLAB_DUO_OAUTH_CLIENT_SECRET કન્ફિગર કરો.", "kilocode": "હાલના OAuth ફ્લો વડે Kilo Code ને કનેક્ટ કરો.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "વિરામ અવધિમાં", "codexPoolUsed": "વપરાયેલ", "codexPoolUntil": "{value} સુધી", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "ગૂઢ ફોલબેક", "anonymousFallbackDesc": "જ્યારે તમામ કન્ફિગર કરેલ કનેક્શનનો ઉપયોગ થઈ જાય છે (ક્વોટા, ક્રેડિટ, અથવા સમાપ્તી), ત્યારે આ પ્રદાતા ની કીલેસ ટિયરનો તાત્કાલિક ઉપયોગ કરો. અનામિક વિનંતીઓ મોકલવા માટે આ પ્રદાતાને છોડી દેવા માટે બંધ કરો - જ્યારે કીલેસ ટિયર તેમને નકારી દે ત્યારે ભલામણ કરવામાં આવે છે (401).", "anonymousFallbackEnabled": "{provider} માટે અજ્ઞાત ફોલબેક સક્રિય છે", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "સાચવેલ મોડેલ અંતિમ બિંદુની સેટિંગ્સ", "searchByModelAria": "મોડલ દ્વારા શોધો", "selectSupportedEndpoint": "કમથી કમ એક સમર્થિત અંતિમ બિંદુ પસંદ કરો", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "આપોઆપ અપસ્ટ્રીમ મોડલ્સ લાવો", - "autoFetchModelsEnabled": "અપસ્ટ્રીમ મોડલ આપોઆપ મેળવવું સક્રિય છે", - "autoFetchModelsDisabled": "અપસ્ટ્રીમ મોડલ આપોઆપ મેળવનાર બંધ છે", - "autoFetchModelsTooltip": "જરૂર પડ્યે અપસ્ટ્રીમ મોડલ્સને લાવવા અને કેશ કરવા", - "autoFetchModelsToggleFailed": "અપસ્ટ્રીમ મોડલ ઓટો-ફેચ ટોગલ કરવામાં નિષ્ફળ થયું", - "overridesUpstreamModel": "અપસ્ટ્રીમને ઓવરરાઈડ કરે છે", - "autoFetchModelsPartialFailure": "કેટલાક કનેક્શન અપડેટ થયા, પરંતુ ઉપરવાળા મોડેલનું ઓટો-ફેચ દરેક જગ્યાએ બદલાયું નથી", - "overridesUpstreamModelHint": "તમારા સેટિંગ્સ આ અપસ્ટ્રીમ મોડેલને ઓવરરાઈડ કરે છે", - "resetToUpstreamDefaultsSuccess": "ઉપરવાળી મોડલ ડિફોલ્ટ્સ પુનઃસ્થાપિત કરવામાં આવ્યા", - "resetToUpstreamDefaults": "અપસ્ટ્રીમ ડિફોલ્ટ્સ પુનઃસ્થાપિત કરો", - "resetToUpstreamDefaultsFailed": "અપસ્ટ્રીમ મોડલ ડિફોલ્ટ્સ પુનઃસ્થાપિત કરવામાં નિષ્ફળ થયું" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "પ્રતિબંધિત કીવર્ડ્સ", "customBannedSignalsDesc": "વધારાના કીવર્ડ્સ જે કાયમી એકાઉન્ટ પ્રતિબંધ શોધને ટ્રિગર કરે છે. બિલ્ટ-ઇન કીવર્ડ્સ હંમેશા લાગુ પડે છે.", "customBannedSignalsPlaceholder": "દા.ત. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "કન્ફિગર કરેલ", "none": "કોઈ નહીં", "modelOverrideValuePlaceholder": "સંખ્યાત્મક મૂલ્ય", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "કી વેલ્યુ ઉમેરો", "noModelOverrides": "આ મોડેલ માટે કોઈ ઓવરરાઇડ્સ કન્ફિગર કરેલ નથી.", "modelOverrideLoadFailed": "મોડેલ ઓવરરાઇડ્સ લોડ કરવામાં નિષ્ફળ", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "સંક્ષિપ્ત CJK (文言)", "description": "ક્લાસિકલ-ચાઇનીઝ અલ્ટ્રા-સંક્ષિપ્ત શૈલી (ફક્ત ચાઇનીઝ માટે ઉપલબ્ધ)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "રાઉન્ડ-રોબિન અને રેન્ડમ કોમ્બોઝ પ્રથમ-સંદેશ હેશ દ્વારા સમગ્ર વાતચીતને એક કનેક્શન પર પિન કરવાને બદલે દરેક વિનંતી પર અલગ કનેક્શન પર ફરે છે. મલ્ટિ-ટર્ન ચેટ્સ માટે પ્રોમ્પ્ટ-કેશ હિટ્સ સાચવવા માટે આને બંધ રાખો. પ્રતિ-કોમ્બો ઓવરરાઇડ્સ અગ્રતા લે છે.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "ઓળખપત્ર રેડેક્શન", "credentialRedactionDesc": "પ્રદાતાઓ તરફ મોકલવામાં આવેલા સંદર્ભમાંથી અને પ્રતિસાદોમાંથી API કી, ટોકન્સ અને સિક્રેટ્સને રેડેક્ટ કરો.", "enableCredentialRedaction": "ઓળખપત્ર રેડેક્શન સક્ષમ કરો", @@ -8621,6 +8623,27 @@ }, "enableTitle": "એન્જિન સક્ષમ કરો", "enableDescription": "સ્ટેકમાં છેલ્લે ચાલે છે (RTK/Caveman લખાણ સાફ કરે તે પછી, OmniGlyph બાકીના ભાગને છબીઓમાં રૂપાંતરિત કરે છે) અને omniglyph મોડ દ્વારા સ્વતંત્ર રીતે પણ ચાલે છે. આ એક પૂર્વાવલોકન છે અને એન્ડ-ટુ-એન્ડ માન્યતા પૂર્ણ ન થાય ત્યાં સુધી ડિફૉલ્ટ રૂપે બંધ રહે છે.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "સાચવ્યું.", "saveFailed": "સાચવી શકાયું નથી.", "enableAria": "OmniGlyph એન્જિન સક્ષમ કરો", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "મહત્તમ", "grokAutoTopUpMonth": "મહિનો", "grokAdditionalCredits": "વધુ ક્રેડિટ", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "પ્રથમ ટોકન", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 764e30527a..90db68373a 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "ציר זמן של בקשות חזותיות", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "פתח", "close": "סגור" }, - "noResults": "אין תוצאות", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "אין תוצאות" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "שיתוף מכסה", "discovery": "גילוי", "freeProviderRankings": "דירוג ספקים חינמיים", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "מסלולים חינמיים", "gamification": "משחוק", "leaderboard": "לוח מובילים", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "ספק זה הוצא משימוש", "riskNotice": { "title": "לפני שממשיכים", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "ספק עם סייגי שימוש — לחץ לפרטים", "oauth": "ספק זה משתמש בסשן המוצר הרשמי/OAuth שלך, שאינו מורשה לשימוש בפרוקסי/נתב. איננו ממליצים על שימוש אינטנסיבי בסוכנים אוטונומיים (בסגנון OpenCloud, תהליכים ארוכים מרובי שלבים, אצוות גדולות) — ספק ה-upstream עלול להגיב בהגבלת החשבון או בחסימתו. השימוש הוא על אחריותך בלבד.", "webCookie": "ספק זה מבצע אימות באמצעות עוגיות סשן הדפדפן שלך. שירות ה-upstream עלול לבטל את תוקף הסשן בכל עת, מה שידרוש ממך להתחבר מחדש. לא מומלץ לפעולות ארוכות ללא השגחה. השימוש הוא על אחריותך בלבד.", @@ -5107,9 +5111,9 @@ "cancel": "ביטול" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "מושבת", "enableProvider": "הפעל ספק", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "מדלג על {count} דגמים קיימים", "autoSync": "סנכרון אוטומטי", "autoSyncShort": "סנכרון", + "autoFetchModels": "משוך אוטומטית מודלים מהמקור", + "autoFetchModelsTooltip": "שחזר ושמור במטמון מודלים עליונים כשצריך", + "autoFetchModelsEnabled": "מודל upstream אוטומטי להורדה מופעל", + "autoFetchModelsDisabled": "איסוף אוטומטי של מודל עליון מושבת", + "autoFetchModelsToggleFailed": "נכשל בהחלפת מצב האיסוף האוטומטי של המודל העליון", + "autoFetchModelsPartialFailure": "כמה חיבורים עודכנו, אך מודל העל לא שונה בכל מקום", + "overridesUpstreamModel": "מעלים על עליון", + "overridesUpstreamModelHint": "ההגדרות שלך עוקפות את המודל העליון הזה", + "resetToUpstreamDefaults": "שחזר את ברירות המחדל של ה-upstream", + "resetToUpstreamDefaultsSuccess": "שוחזרו ברירות המחדל של המודל העליון", + "resetToUpstreamDefaultsFailed": "נכשל בשחזור ברירות המחדל של המודל העליון", "autoSyncTooltip": "רענן אוטומטית את רשימת הדגמים כל 24 שעות (ניתן להגדרה באמצעות MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "סנכרון אוטומטי מופעל - הדגמים יתרעננו מעת לעת", "autoSyncDisabled": "הסנכרון האוטומטי מושבת", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "שכתוב קריאות כלי web_fetch מובנות ל-/v1/web/fetch של OmniRoute.", "interceptionLoadError": "טעינת הגדרות היירוט נכשלה: {error}", "interceptionSaveError": "שמירת הגדרות היירוט נכשלה: {error}", - "ccAliasSectionTitle": "חשוף בקוד קלוד (claude/…)", - "ccAliasSectionHint": "פרסם את המודלים של ספק זה תחת claude/<provider>/<model> מזהי מראה כך שגילוי המודלים של שער קוד קלוד יוכל לרשום אותם. כבוי כברירת מחדל — הפעלת זה מכפילה את רשומות הקטלוג עבור כל הלקוחות.", - "ccAliasProviderLevelLabel": "ברירת מחדל של ספק", - "ccAliasModelOverridesLabel": "הגדרות לפי מודל", - "ccAliasModelOverrideAriaLabel": "החלפה עבור {modelId}", - "ccAliasStateInherit": "ירש", - "ccAliasStateOn": "על", - "ccAliasStateOff": "כבוי", - "ccAliasAddModelPlaceholder": "מזהה מודל (למשל, gpt-4o)", - "ccAliasAddModelButton": "הוסף ע override", - "ccAliasLoadError": "לא הצלחנו לטעון את הגדרות discovery-alias: {error}", - "ccAliasSaveError": "שגיאה בשמירת הגדרת discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "חבר את Galadriel באמצעות מפתח API.", "predibase": "קרדיט ניסיון חינם בסך $25 (תוקף ל-30 יום)", "chenzk": "שער תואם OpenAI עם קטלוג מודלים חי ב-chenzk.top.", - "freepik": "צור תמונות באמצעות ה-API של Mystic מבית Freepik.", + "magnific": "צור תמונות באמצעות ה-API של Mystic מבית Freepik.", "freetheai": "שער חינמי תואם OpenAI עם תמיכה במודלים בשיטת passthrough.", "g4f-gemini": "פרוקסי הפוך חינמי ללא מפתח מ-g4f.space ל-Gemini, מוגבל ל-5 בקשות לדקה.", "g4f-groq": "פרוקסי הפוך חינמי ללא מפתח מ-g4f.space ל-Groq, מוגבל ל-5 בקשות לדקה.", @@ -6209,6 +6224,7 @@ "claude": "חבר את Claude Code באמצעות תהליך ה-OAuth הקיים.", "cline": "חבר את Cline באמצעות תהליך ה-OAuth הקיים.", "cursor": "חבר את Cursor IDE באמצעות תהליך ה-OAuth הקיים.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "חבר את GitHub Copilot באמצעות תהליך ה-OAuth הקיים.", "gitlab-duo": "יישום OAuth עם הרשאות (scopes) של ai_features + read_user. הגדר את GITLAB_DUO_OAUTH_CLIENT_ID ואופציונלית את GITLAB_DUO_OAUTH_CLIENT_SECRET במופע OmniRoute זה.", "kilocode": "חבר את Kilo Code באמצעות תהליך ה-OAuth הקיים.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "בתקופת המתנה", "codexPoolUsed": "בשימוש", "codexPoolUntil": "עד {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "נפילה אנונימית", "anonymousFallbackDesc": "כאשר כל החיבורים המוגדרים נוצלו (מכסה, אשראי או תאריך תפוגה), השתמש זמנית בשכבת ללא מפתח של ספק זה. כבה כדי לדלג על ספק זה במקום לשלוח בקשות אנונימיות - מומלץ כאשר שכבת ללא מפתח דוחה אותן (401).", "anonymousFallbackEnabled": "גיבוי אנונימי מופעל עבור {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "הגדרות נקודת הקצה של המודל השמור", "searchByModelAria": "חפש לפי דגם", "selectSupportedEndpoint": "בחר לפחות נקודת קצה אחת נתמכת", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "איסוף אוטומטי של מודל עליון מושבת", - "autoFetchModelsEnabled": "מודל upstream אוטומטי להורדה מופעל", - "autoFetchModels": "משוך אוטומטית מודלים מהמקור", - "autoFetchModelsTooltip": "שחזר ושמור במטמון מודלים עליונים כשצריך", - "autoFetchModelsToggleFailed": "נכשל בהחלפת מצב האיסוף האוטומטי של המודל העליון", - "autoFetchModelsPartialFailure": "כמה חיבורים עודכנו, אך מודל העל לא שונה בכל מקום", - "overridesUpstreamModel": "מעלים על עליון", - "overridesUpstreamModelHint": "ההגדרות שלך עוקפות את המודל העליון הזה", - "resetToUpstreamDefaults": "שחזר את ברירות המחדל של ה-upstream", - "resetToUpstreamDefaultsFailed": "נכשל בשחזור ברירות המחדל של המודל העליון", - "resetToUpstreamDefaultsSuccess": "שוחזרו ברירות המחדל של המודל העליון" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "הגדרות", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "מילות מפתח חסומות", "customBannedSignalsDesc": "מילות מפתח נוספות שמפעילות זיהוי לחסימה קבועה של החשבון. מילות מפתח מובנות חלות תמיד.", "customBannedSignalsPlaceholder": "לדוגמה: api key revoked", @@ -7210,6 +7203,7 @@ "configured": "מוגדר", "none": "ללא", "modelOverrideValuePlaceholder": "ערך מספרי", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "הוסף מפתח-ערך", "noModelOverrides": "לא הוגדרו דריסות עבור מודל זה.", "modelOverrideLoadFailed": "טעינת דריסות המודל נכשלה", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK תמציתי (文言)", "description": "סגנון סיני קלאסי אולטרה-תמציתי (זמין עבור סינית בלבד)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "שילובי Round-robin ושילובים אקראיים עוברים לחיבור שונה בכל בקשה במקום להצמיד שיחה שלמה לחיבור יחיד לפי ה-hash של ההודעה הראשונה. השאר כבוי כדי לשמר פגיעות ב-prompt-cache עבור שיחות מרובות סבבים. דריסות ברמת השילוב מקבלות קדימות.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "הסתרת פרטי אימות", "credentialRedactionDesc": "הסתרת מפתחות API, טוקנים וסודות מההקשר הנשלח לספקים ומהתגובות.", "enableCredentialRedaction": "הפעלת הסתרת פרטי אימות", @@ -8621,6 +8623,27 @@ }, "enableTitle": "הפעל את המנוע", "enableDescription": "רץ אחרון במחסנית (לאחר ש-RTK/Caveman מנקה את הטקסט, OmniGlyph ממיר את השאר לתמונות) ורץ גם באופן עצמאי דרך מצב omniglyph. זוהי תצוגה מקדימה והיא נשארת כבויה כברירת מחדל עד להשלמת אימות מקצה לקצה.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "נשמר.", "saveFailed": "לא ניתן היה לשמור.", "enableAria": "הפעל את מנוע OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "מקסימום", "grokAutoTopUpMonth": "חודש", "grokAdditionalCredits": "קרדיטים נוספים", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "לוגר", "proxyTab": "פרוקסי", "budgetManagement": "ניהול תקציב", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "טוקן ראשון", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index bf73a186fc..9e353a269b 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "दृश्य अनुरोध समयरेखा", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "खोलें", "close": "बंद करें" }, - "noResults": "कोई परिणाम नहीं", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "कोई परिणाम नहीं" }, "webhooks": { "title": "वेबहुक", @@ -1739,8 +1739,8 @@ "quotaShare": "कोटा शेयर", "discovery": "खोज", "freeProviderRankings": "मुफ़्त प्रदाता रैंकिंग", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "मुफ़्त टियर", "gamification": "गेमीफिकेशन", "leaderboard": "लीडरबोर्ड", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "इस प्रदाता को अस्वीकृत कर दिया गया है", "riskNotice": { "title": "जारी रखने से पहले", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "उपयोग संबंधी चेतावनियों वाला प्रदाता — विवरण के लिए क्लिक करें", "oauth": "यह प्रदाता आपके आधिकारिक उत्पाद सत्र/OAuth का उपयोग करता है, जो प्रॉक्सी/राउटर उपयोग के लिए अधिकृत नहीं है। हम गहन स्वायत्त एजेंट उपयोग (OpenCloud-शैली, लंबे बहु-चरणीय प्रवाह, बड़े बैच) की अनुशंसा नहीं करते हैं — अपस्ट्रीम खाते को प्रतिबंधित या ब्लॉक करके प्रतिक्रिया दे सकता है। अपने जोखिम पर उपयोग करें।", "webCookie": "यह प्रदाता आपके वेब सत्र कुकीज़ के माध्यम से प्रमाणित करता है। अपस्ट्रीम सेवा किसी भी समय सत्र को अमान्य कर सकती है, जिससे आपको फिर से लॉग इन करने की आवश्यकता होगी। लंबे समय तक बिना निगरानी वाले संचालन के लिए अनुशंसित नहीं है। अपने जोखिम पर उपयोग करें।", @@ -5107,9 +5111,9 @@ "cancel": "रद्द करें" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "अक्षम", "enableProvider": "प्रदाता सक्षम करें", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "{count} मौजूदा मॉडल छोड़े जा रहे हैं", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "स्वचालित रूप से अपस्ट्रीम मॉडल लाएं", + "autoFetchModelsTooltip": "आवश्यक होने पर अपस्ट्रीम मॉडल लाएं और कैश करें", + "autoFetchModelsEnabled": "उपधारा मॉडल स्वचालित-लाने की सुविधा सक्षम है", + "autoFetchModelsDisabled": "उपधारा मॉडल ऑटो-फेच अक्षम किया गया", + "autoFetchModelsToggleFailed": "उपस्ट्रीम मॉडल ऑटो-फेच को टॉगल करने में विफल रहा", + "autoFetchModelsPartialFailure": "कुछ कनेक्शन अपडेट किए गए, लेकिन अपस्ट्रीम मॉडल ऑटो-फेच हर जगह नहीं बदला", + "overridesUpstreamModel": "उपस्ट्रीम को ओवरराइड करता है", + "overridesUpstreamModelHint": "आपकी सेटिंग्स इस अपस्ट्रीम मॉडल को ओवरराइड करती हैं", + "resetToUpstreamDefaults": "उपधारा डिफ़ॉल्ट्स को पुनर्स्थापित करें", + "resetToUpstreamDefaultsSuccess": "उपधारा मॉडल डिफ़ॉल्ट्स को पुनर्स्थापित किया गया", + "resetToUpstreamDefaultsFailed": "उपधारा मॉडल डिफ़ॉल्ट्स को पुनर्स्थापित करने में विफल", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "मूल web_fetch टूल कॉल को OmniRoute के /v1/web/fetch पर रीराइट करें।", "interceptionLoadError": "इंटरसेप्शन सेटिंग्स लोड करने में विफल: {error}", "interceptionSaveError": "इंटरसेप्शन सेटिंग्स सहेजने में विफल: {error}", - "ccAliasSectionTitle": "Claude कोड में एक्सपोज़ करें (claude/…)", - "ccAliasSectionHint": "इस प्रदाता के मॉडल को claude/<provider>/<model> मिरर आईडी के तहत विज्ञापित करें ताकि Claude Code का गेटवे मॉडल खोज उन्हें सूचीबद्ध कर सके। डिफ़ॉल्ट रूप से बंद — इसे सक्षम करने से सभी ग्राहकों के लिए कैटलॉग प्रविष्टियाँ दोगुनी हो जाती हैं।", - "ccAliasProviderLevelLabel": "प्रदाता डिफ़ॉल्ट", - "ccAliasModelOverridesLabel": "प्रति-मॉडल ओवरराइड्स", - "ccAliasModelOverrideAriaLabel": "{modelId} के लिए ओवरराइड", - "ccAliasStateInherit": "विरासत", - "ccAliasStateOn": "चालू", - "ccAliasStateOff": "बंद", - "ccAliasAddModelPlaceholder": "मॉडल आईडी (जैसे gpt-4o)", - "ccAliasAddModelButton": "ओवरराइड जोड़ें", - "ccAliasLoadError": "डिस्कवरी-एलियस सेटिंग्स लोड करने में विफल: {error}", - "ccAliasSaveError": "डिस्कवरी-उपनाम सेटिंग को सहेजने में विफल: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "API कुंजी के साथ Galadriel को कनेक्ट करें।", "predibase": "$25 मुफ्त ट्रायल क्रेडिट (30 दिनों की वैधता)", "chenzk": "chenzk.top पर लाइव मॉडल कैटलॉग के साथ OpenAI-संगत गेटवे।", - "freepik": "Freepik के Mystic API के साथ चित्र जनरेट करें।", + "magnific": "Freepik के Mystic API के साथ चित्र जनरेट करें।", "freetheai": "पासथ्रू मॉडल समर्थन के साथ मुफ्त OpenAI-संगत गेटवे।", "g4f-gemini": "Gemini के लिए मुफ्त बिना-कुंजी वाला g4f.space रिवर्स प्रॉक्सी, प्रति मिनट 5 अनुरोधों तक सीमित।", "g4f-groq": "Groq के लिए मुफ्त बिना-कुंजी वाला g4f.space रिवर्स प्रॉक्सी, प्रति मिनट 5 अनुरोधों तक सीमित।", @@ -6209,6 +6224,7 @@ "claude": "मौजूदा OAuth फ़्लो के साथ Claude Code को कनेक्ट करें।", "cline": "मौजूदा OAuth फ़्लो के साथ Cline को कनेक्ट करें।", "cursor": "मौजूदा OAuth फ़्लो के साथ Cursor IDE को कनेक्ट करें।", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "मौजूदा OAuth फ़्लो के साथ GitHub Copilot को कनेक्ट करें।", "gitlab-duo": "ai_features + read_user स्कोप के साथ OAuth एप्लिकेशन। इस OmniRoute इंस्टेंस पर GITLAB_DUO_OAUTH_CLIENT_ID और वैकल्पिक रूप से GITLAB_DUO_OAUTH_CLIENT_SECRET कॉन्फ़िगर करें।", "kilocode": "मौजूदा OAuth फ़्लो के साथ Kilo Code को कनेक्ट करें।", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "कूलडाउन जारी", "codexPoolUsed": "उपयोग किया गया", "codexPoolUntil": "{value} तक", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "गुमनाम बैकअप", "anonymousFallbackDesc": "जब सभी कॉन्फ़िगर की गई कनेक्शन समाप्त हो जाते हैं (कोटा, क्रेडिट, या समाप्ति), तो अस्थायी रूप से इस प्रदाता की कीलेस श्रेणी का उपयोग करें। इस प्रदाता को छोड़ने के लिए बंद करें बजाय गुमनाम अनुरोध भेजने के — जब कीलेस श्रेणी उन्हें अस्वीकार करती है (401) तो यह अनुशंसित है।", "anonymousFallbackEnabled": "{provider} के लिए गुमनाम फॉलबैक सक्षम किया गया", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "सहेजे गए मॉडल एंडपॉइंट सेटिंग्स", "searchByModelAria": "मॉडल द्वारा खोजें", "selectSupportedEndpoint": "कम से कम एक समर्थित एंडपॉइंट चुनें", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "स्वचालित रूप से अपस्ट्रीम मॉडल लाएं", - "autoFetchModelsEnabled": "उपधारा मॉडल स्वचालित-लाने की सुविधा सक्षम है", - "autoFetchModelsDisabled": "उपधारा मॉडल ऑटो-फेच अक्षम किया गया", - "autoFetchModelsTooltip": "आवश्यक होने पर अपस्ट्रीम मॉडल लाएं और कैश करें", - "overridesUpstreamModel": "उपस्ट्रीम को ओवरराइड करता है", - "autoFetchModelsToggleFailed": "उपस्ट्रीम मॉडल ऑटो-फेच को टॉगल करने में विफल रहा", - "overridesUpstreamModelHint": "आपकी सेटिंग्स इस अपस्ट्रीम मॉडल को ओवरराइड करती हैं", - "autoFetchModelsPartialFailure": "कुछ कनेक्शन अपडेट किए गए, लेकिन अपस्ट्रीम मॉडल ऑटो-फेच हर जगह नहीं बदला", - "resetToUpstreamDefaults": "उपधारा डिफ़ॉल्ट्स को पुनर्स्थापित करें", - "resetToUpstreamDefaultsSuccess": "उपधारा मॉडल डिफ़ॉल्ट्स को पुनर्स्थापित किया गया", - "resetToUpstreamDefaultsFailed": "उपधारा मॉडल डिफ़ॉल्ट्स को पुनर्स्थापित करने में विफल" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "सेटिंग्स", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "प्रतिबंधित कीवर्ड", "customBannedSignalsDesc": "अतिरिक्त कीवर्ड जो स्थायी खाता प्रतिबंध पहचान को ट्रिगर करते हैं। अंतर्निहित कीवर्ड हमेशा लागू होते हैं।", "customBannedSignalsPlaceholder": "उदा. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "कॉन्फ़िगर किया गया", "none": "कोई नहीं", "modelOverrideValuePlaceholder": "संख्यात्मक मान", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "कुंजी मान जोड़ें", "noModelOverrides": "इस मॉडल के लिए कोई ओवरराइड कॉन्फ़िगर नहीं किया गया है।", "modelOverrideLoadFailed": "मॉडल ओवरराइड लोड करने में विफल", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "संक्षिप्त CJK (文言)", "description": "शास्त्रीय-चीनी अति-संक्षिप्त शैली (केवल चीनी भाषा के लिए उपलब्ध)।" @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "राउंड-रॉबिन और रैंडम कॉम्बो पहले-संदेश हैश द्वारा पूरी बातचीत को एक कनेक्शन पर पिन करने के बजाय हर अनुरोध पर एक अलग कनेक्शन पर रोटेट होते हैं। मल्टी-टर्न चैट के लिए प्रॉम्प्ट-कैश हिट्स को बनाए रखने के लिए इसे बंद रखें। प्रति-कॉम्बो ओवरराइड को प्राथमिकता दी जाती है।", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "क्रेडेंशियल रिडैक्शन", "credentialRedactionDesc": "प्रदाताओं को भेजे गए संदर्भ और प्रतिक्रियाओं से API keys, tokens और secrets को रिडैक्ट करें।", "enableCredentialRedaction": "क्रेडेंशियल रिडैक्शन सक्षम करें", @@ -8621,6 +8623,27 @@ }, "enableTitle": "इंजन सक्षम करें", "enableDescription": "स्टैक में सबसे अंत में चलता है (RTK/Caveman द्वारा टेक्स्ट साफ़ करने के बाद, OmniGlyph शेष को छवियों में परिवर्तित करता है) और omniglyph मोड के माध्यम से स्टैंडअलोन भी चलता है। यह एक पूर्वावलोकन है और एंड-टू-एंड सत्यापन पूरा होने तक डिफ़ॉल्ट रूप से बंद रहता है।", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "सहेजा गया।", "saveFailed": "सहेजा नहीं जा सका।", "enableAria": "OmniGlyph इंजन सक्षम करें", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "अधिकतम", "grokAutoTopUpMonth": "महीना", "grokAdditionalCredits": "अतिरिक्त श्रेय", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "लकड़हारा", "proxyTab": "प्रॉक्सी", "budgetManagement": "बजट प्रबंधन", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "पहला टोकन", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index e5ccd837b3..4f8ee2278b 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Vizuális kérelem idővonal", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "megnyitás", "close": "bezárás" }, - "noResults": "Nincs találat", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Nincs találat" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kvótamegosztás", "discovery": "Felfedezés", "freeProviderRankings": "Ingyenes szolgáltatók rangsora", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Ingyenes szintek", "gamification": "Játékosítás", "leaderboard": "Ranglista", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Ez a szolgáltató elavult", "riskNotice": { "title": "Mielőtt folytatná", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Használati figyelmeztetésekkel rendelkező szolgáltató — kattintson a részletekért", "oauth": "Ez a szolgáltató a hivatalos termékmunkamenetet/OAuth-ot használja, amely nem engedélyezett proxy/router használatra. Nem javasoljuk az intenzív autonóm ágens használatot (OpenCloud-stílusú, hosszú, többlépéses folyamatok, nagy kötegek) — az upstream szolgáltató a fiók korlátozásával vagy kitiltásával reagálhat. Saját felelősségre használja.", "webCookie": "Ez a szolgáltató a webes munkamenet-sütik segítségével hitelesít. Az upstream szolgáltatás bármikor érvénytelenítheti a munkamenetet, ami újbóli bejelentkezést igényel. Hosszú, felügyelet nélküli műveletekhez nem ajánlott. Saját felelősségre használja.", @@ -5107,9 +5111,9 @@ "cancel": "Mégse" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Letiltva", "enableProvider": "Szolgáltató engedélyezése", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "{count} meglévő modell kihagyása", "autoSync": "Automatikus szinkronizálás", "autoSyncShort": "Szinkronizálás", + "autoFetchModels": "Automatikus frissítés a felfelé irányuló modellekből", + "autoFetchModelsTooltip": "Töltse le és tárolja a feljebb lévő modelleket, amikor szükséges", + "autoFetchModelsEnabled": "Felfelé irányuló modell automatikus lekérése engedélyezve", + "autoFetchModelsDisabled": "Felfelé irányuló modell automatikus lekérése letiltva", + "autoFetchModelsToggleFailed": "Nem sikerült átkapcsolni a feljebb lévő modell automatikus lekérdezését", + "autoFetchModelsPartialFailure": "Néhány kapcsolat frissítve lett, de a felfelé irányuló modell automatikus lekérése nem változott meg mindenhol", + "overridesUpstreamModel": "Felülírja a felfelé irányuló változtatásokat", + "overridesUpstreamModelHint": "A beállításai felülírják ezt a fenti modellt", + "resetToUpstreamDefaults": "Állítsa vissza az alapértelmezett beállításokat", + "resetToUpstreamDefaultsSuccess": "Visszaállítottuk az upstream modell alapértelmezett beállításait", + "resetToUpstreamDefaultsFailed": "Nem sikerült visszaállítani a fenti modell alapértelmezett beállításait", "autoSyncTooltip": "A modelllista automatikus frissítése 24 óránként (konfigurálható: MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatikus szinkronizálás engedélyezve – a modellek rendszeresen frissülnek", "autoSyncDisabled": "Az automatikus szinkronizálás letiltva", @@ -5439,17 +5454,17 @@ "interceptionLoadError": "Nem sikerült betölteni az elfogási beállításokat: {error}", "interceptionSaveError": "Nem sikerült menteni az elfogási beállításokat: {error}", "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "Hirdesse meg ennek a szolgáltatónak a modelljeit a claude/<provider>/<model> tükörazonosítók alatt, hogy a Claude Code átjáró modell felfedezése listázhassa őket. Alapértelmezés szerint ki van kapcsolva — ennek engedélyezése megduplázza a katalógusbejegyzéseket minden ügyfél számára.", - "ccAliasProviderLevelLabel": "Szolgáltató alapértelmezett", - "ccAliasModelOverridesLabel": "Per-modell felülírások", - "ccAliasModelOverrideAriaLabel": "Felülírás a(z) {modelId} számára", - "ccAliasStateInherit": "Örököl", - "ccAliasStateOn": "Be- és kikapcsolás", - "ccAliasStateOff": "Ki", - "ccAliasAddModelPlaceholder": "Modell azonosító (pl. gpt-4o)", - "ccAliasAddModelButton": "Add Override", - "ccAliasLoadError": "Nem sikerült betölteni a discovery-alias beállításokat: {error}", - "ccAliasSaveError": "Nem sikerült elmenteni a discovery-alias beállítást: {error}", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "A Galadriel összekapcsolása egy API-kulccsal.", "predibase": "25 $ ingyenes próbaverziós kredit (30 napos érvényesség)", "chenzk": "OpenAI-kompatibilis átjáró élő modellkatalógussal a chenzk.top címen.", - "freepik": "Képek generálása a Freepik Mystic API-jával.", + "magnific": "Képek generálása a Freepik Mystic API-jával.", "freetheai": "Ingyenes OpenAI-kompatibilis átjáró átmenő (passthrough) modelltámogatással.", "g4f-gemini": "Ingyenes, kulcs nélküli g4f.space fordított proxy a Geminihez, percenként legfeljebb 5 kéréssel.", "g4f-groq": "Ingyenes, kulcs nélküli g4f.space fordított proxy a Groq-hoz, percenként legfeljebb 5 kéréssel.", @@ -6209,6 +6224,7 @@ "claude": "A Claude Code összekapcsolása a meglévő OAuth-folyamattal.", "cline": "A Cline összekapcsolása a meglévő OAuth-folyamattal.", "cursor": "A Cursor IDE összekapcsolása a meglévő OAuth-folyamattal.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "A GitHub Copilot összekapcsolása a meglévő OAuth-folyamattal.", "gitlab-duo": "OAuth alkalmazás ai_features + read_user hatókörökkel. Konfigurálja a GITLAB_DUO_OAUTH_CLIENT_ID és opcionálisan a GITLAB_DUO_OAUTH_CLIENT_SECRET változókat ezen az OmniRoute példányon.", "kilocode": "A Kilo Code összekapcsolása a meglévő OAuth-folyamattal.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Várakozási időszakban", "codexPoolUsed": "felhasználva", "codexPoolUntil": "Eddig: {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Névtelen visszaesés", "anonymousFallbackDesc": "Amikor az összes konfigurált kapcsolat kimerült (kvóta, kreditek vagy lejárat), ideiglenesen használja ezt a szolgáltató kulcs nélküli szintjét. Kapcsolja ki, hogy kihagyja ezt a szolgáltatót a névtelen kérések küldése helyett — ajánlott, ha a kulcs nélküli szint elutasítja őket (401).", "anonymousFallbackEnabled": "Névtelen visszaesés engedélyezve a(z) {provider} számára", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Mentett modell végpont beállításai", "searchByModelAria": "Keresés modell szerint", "selectSupportedEndpoint": "Válasszon ki legalább egy támogatott végpontot", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Felfelé irányuló modell automatikus lekérése letiltva", - "autoFetchModelsEnabled": "Felfelé irányuló modell automatikus lekérése engedélyezve", - "autoFetchModelsTooltip": "Töltse le és tárolja a feljebb lévő modelleket, amikor szükséges", - "autoFetchModels": "Automatikus frissítés a felfelé irányuló modellekből", - "autoFetchModelsToggleFailed": "Nem sikerült átkapcsolni a feljebb lévő modell automatikus lekérdezését", - "autoFetchModelsPartialFailure": "Néhány kapcsolat frissítve lett, de a felfelé irányuló modell automatikus lekérése nem változott meg mindenhol", - "overridesUpstreamModel": "Felülírja a felfelé irányuló változtatásokat", - "overridesUpstreamModelHint": "A beállításai felülírják ezt a fenti modellt", - "resetToUpstreamDefaults": "Állítsa vissza az alapértelmezett beállításokat", - "resetToUpstreamDefaultsSuccess": "Visszaállítottuk az upstream modell alapértelmezett beállításait", - "resetToUpstreamDefaultsFailed": "Nem sikerült visszaállítani a fenti modell alapértelmezett beállításait" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Beállítások elemre", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Tiltott kulcsszavak", "customBannedSignalsDesc": "További kulcsszavak, amelyek végleges fióktiltás észlelését váltják ki. A beépített kulcsszavak mindig érvényesek.", "customBannedSignalsPlaceholder": "pl. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "konfigurálva", "none": "Nincs", "modelOverrideValuePlaceholder": "Numerikus érték", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Kulcs-érték hozzáadása", "noModelOverrides": "Nincsenek felülírások konfigurálva ehhez a modellhez.", "modelOverrideLoadFailed": "Nem sikerült betölteni a modellfelülírásokat", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Tömör CJK (文言)", "description": "Klasszikus kínai ultratömör stílus (csak kínai nyelven érhető el)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "A round-robin és a véletlenszerű kombinációk minden kérésnél új kapcsolatra váltanak, ahelyett, hogy a teljes beszélgetést egyetlen kapcsolathoz rögzítenék az első üzenet hash-e alapján. Hagyja kikapcsolva, hogy megőrizze a prompt-cache találatokat a többfordulós csevegéseknél. A kombinációnkénti felülírások elsőbbséget élveznek.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Hitelesítési adatok kitakarása", "credentialRedactionDesc": "API-kulcsok, tokenek és titkok kitakarása a szolgáltatóknak küldött kontextusból és a válaszokból.", "enableCredentialRedaction": "Hitelesítési adatok kitakarásának engedélyezése", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Motor engedélyezése", "enableDescription": "Utolsóként fut a veremben (miután az RTK/Caveman megtisztítja a szöveget, az OmniGlyph képekké alakítja a maradékot), valamint önállóan is fut a omniglyph módon keresztül. Ez egy előnézet, és alapértelmezés szerint kikapcsolva marad a végpontok közötti ellenőrzés befejezéséig.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Mentve.", "saveFailed": "Nem sikerült menteni.", "enableAria": "Az OmniGlyph motor engedélyezése", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "hónap", "grokAdditionalCredits": "További Kiadások", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Költségvetési menedzsment", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Első token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 352daafd83..bc841a35d6 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Garis waktu permintaan visual", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "buka", "close": "tutup" }, - "noResults": "Tidak ada hasil", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Tidak ada hasil" }, "webhooks": { "title": "Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "Pangsa Kuota", "discovery": "Penemuan", "freeProviderRankings": "Peringkat Penyedia Gratis", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Tingkat Gratis", "gamification": "Gamifikasi", "leaderboard": "Papan Peringkat", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Penyedia ini sudah tidak digunakan lagi", "riskNotice": { "title": "Sebelum melanjutkan", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Penyedia dengan catatan penggunaan — klik untuk detail", "oauth": "Penyedia ini menggunakan sesi/OAuth produk resmi Anda, yang tidak diizinkan untuk penggunaan proxy/router. Kami tidak menyarankan penggunaan agen otonom yang intensif (gaya OpenCloud, alur multi-langkah yang panjang, batch besar) — upstream dapat bereaksi dengan membatasi atau memblokir akun. Gunakan dengan risiko Anda sendiri.", "webCookie": "Penyedia ini mengautentikasi melalui cookie sesi web Anda. Layanan upstream dapat membatalkan sesi kapan saja, mengharuskan Anda untuk masuk kembali. Tidak disarankan untuk operasi tanpa pengawasan yang lama. Gunakan dengan risiko Anda sendiri.", @@ -5107,9 +5111,9 @@ "cancel": "Batal" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Dengan disabilitas", "enableProvider": "Aktifkan penyedia", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Melewatkan {count} model yang sudah ada", "autoSync": "Sinkronisasi Otomatis", "autoSyncShort": "Sinkronkan", + "autoFetchModels": "Ambil model upstream secara otomatis", + "autoFetchModelsTooltip": "Ambil dan simpan model upstream saat diperlukan", + "autoFetchModelsEnabled": "Model upstream auto-fetch diaktifkan", + "autoFetchModelsDisabled": "Pengambilan otomatis model upstream dinonaktifkan", + "autoFetchModelsToggleFailed": "Gagal untuk mengubah pengambilan otomatis model upstream", + "autoFetchModelsPartialFailure": "Beberapa koneksi diperbarui, tetapi pengambilan otomatis model upstream tidak berubah di semua tempat", + "overridesUpstreamModel": "Mengganti upstream", + "overridesUpstreamModelHint": "Pengaturan Anda menimpa model upstream ini", + "resetToUpstreamDefaults": "Pulihkan pengaturan default upstream", + "resetToUpstreamDefaultsSuccess": "Mengembalikan pengaturan default model upstream", + "resetToUpstreamDefaultsFailed": "Gagal mengembalikan pengaturan model upstream ke default", "autoSyncTooltip": "Segarkan daftar model secara otomatis setiap 24 jam (dapat dikonfigurasi melalui MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Sinkronisasi otomatis diaktifkan — model akan disegarkan secara berkala", "autoSyncDisabled": "Sinkronisasi otomatis dinonaktifkan", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Tulis ulang panggilan alat web_fetch bawaan ke /v1/web/fetch milik OmniRoute.", "interceptionLoadError": "Gagal memuat pengaturan intersepsi: {error}", "interceptionSaveError": "Gagal menyimpan pengaturan intersepsi: {error}", - "ccAliasSectionTitle": "Expose di Claude Code (claude/…)", - "ccAliasSectionHint": "Iklankan model penyedia ini di bawah claude/<provider>/<model> ID cermin agar penemuan model gateway Claude Code dapat mencantumkannya. Mati secara default — mengaktifkan ini menggandakan entri katalog untuk semua klien.", - "ccAliasProviderLevelLabel": "Penyedia default", - "ccAliasModelOverridesLabel": "Penggantian per-model", - "ccAliasModelOverrideAriaLabel": "Override untuk {modelId}", - "ccAliasStateInherit": "Warisi", - "ccAliasStateOn": "Hidup", - "ccAliasStateOff": "Matikan", - "ccAliasAddModelPlaceholder": "Model id (misalnya gpt-4o)", - "ccAliasAddModelButton": "Tambahkan override", - "ccAliasLoadError": "Gagal memuat pengaturan discovery-alias: {error}", - "ccAliasSaveError": "Gagal menyimpan pengaturan discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Hubungkan Galadriel dengan kunci API.", "predibase": "Kredit uji coba gratis $25 (validitas 30 hari)", "chenzk": "Gateway yang kompatibel dengan OpenAI dengan katalog model langsung di chenzk.top.", - "freepik": "Hasilkan gambar dengan Mystic API dari Freepik.", + "magnific": "Hasilkan gambar dengan Mystic API dari Freepik.", "freetheai": "Gateway gratis yang kompatibel dengan OpenAI dengan dukungan model passthrough.", "g4f-gemini": "Proksi terbalik g4f.space tanpa kunci gratis ke Gemini, dibatasi hingga 5 permintaan per menit.", "g4f-groq": "Proksi terbalik g4f.space tanpa kunci gratis ke Groq, dibatasi hingga 5 permintaan per menit.", @@ -6209,6 +6224,7 @@ "claude": "Hubungkan Claude Code dengan alur OAuth yang ada.", "cline": "Hubungkan Cline dengan alur OAuth yang ada.", "cursor": "Hubungkan Cursor IDE dengan alur OAuth yang ada.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Hubungkan GitHub Copilot dengan alur OAuth yang ada.", "gitlab-duo": "Aplikasi OAuth dengan cakupan ai_features + read_user. Konfigurasikan GITLAB_DUO_OAUTH_CLIENT_ID dan secara opsional GITLAB_DUO_OAUTH_CLIENT_SECRET pada instans OmniRoute ini.", "kilocode": "Hubungkan Kilo Code dengan alur OAuth yang ada.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Dalam masa tunggu", "codexPoolUsed": "terpakai", "codexPoolUntil": "Hingga {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback anonim", "anonymousFallbackDesc": "Ketika semua koneksi yang dikonfigurasi habis (kuota, kredit, atau masa berlaku), gunakan sementara tingkat tanpa kunci penyedia ini. Matikan untuk melewati penyedia ini alih-alih mengirim permintaan anonim — disarankan ketika tingkat tanpa kunci menolak permintaan tersebut (401).", "anonymousFallbackEnabled": "Fallback anonim diaktifkan untuk {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Pengaturan endpoint model yang disimpan", "searchByModelAria": "Cari berdasarkan model", "selectSupportedEndpoint": "Pilih setidaknya satu endpoint yang didukung", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Ambil model upstream secara otomatis", - "autoFetchModelsEnabled": "Model upstream auto-fetch diaktifkan", - "autoFetchModelsTooltip": "Ambil dan simpan model upstream saat diperlukan", - "autoFetchModelsDisabled": "Pengambilan otomatis model upstream dinonaktifkan", - "autoFetchModelsToggleFailed": "Gagal untuk mengubah pengambilan otomatis model upstream", - "overridesUpstreamModel": "Mengganti upstream", - "autoFetchModelsPartialFailure": "Beberapa koneksi diperbarui, tetapi pengambilan otomatis model upstream tidak berubah di semua tempat", - "resetToUpstreamDefaults": "Pulihkan pengaturan default upstream", - "overridesUpstreamModelHint": "Pengaturan Anda menimpa model upstream ini", - "resetToUpstreamDefaultsSuccess": "Mengembalikan pengaturan default model upstream", - "resetToUpstreamDefaultsFailed": "Gagal mengembalikan pengaturan model upstream ke default" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Pengaturan", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Kata Kunci yang Diblokir", "customBannedSignalsDesc": "Kata kunci tambahan yang memicu deteksi pemblokiran akun permanen. Kata kunci bawaan selalu berlaku.", "customBannedSignalsPlaceholder": "mis. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "dikonfigurasi", "none": "Tidak ada", "modelOverrideValuePlaceholder": "Nilai numerik", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Tambahkan nilai kunci", "noModelOverrides": "Tidak ada override yang dikonfigurasi untuk model ini.", "modelOverrideLoadFailed": "Gagal memuat override model", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Tionghoa Klasik (hanya tersedia untuk bahasa Tionghoa)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Kombo round-robin dan acak berganti ke koneksi yang berbeda pada setiap permintaan alih-alih menyematkan seluruh percakapan ke satu koneksi berdasarkan hash pesan pertama. Biarkan nonaktif untuk mempertahankan hit prompt-cache pada obrolan multi-turn. Penggantian per-kombo lebih diutamakan.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Redaksi Kredensial", "credentialRedactionDesc": "Redaksi kunci API, token, dan rahasia dari konteks yang dikirim ke penyedia dan dari respons.", "enableCredentialRedaction": "Aktifkan redaksi kredensial", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Aktifkan mesin", "enableDescription": "Berjalan terakhir dalam tumpukan (setelah RTK/Caveman membersihkan teks, OmniGlyph mengonversi sisanya menjadi gambar) dan juga berjalan mandiri melalui mode omniglyph. Ini adalah pratinjau dan tetap dinonaktifkan secara default hingga validasi menyeluruh selesai.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Disimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Aktifkan mesin OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksimum", "grokAutoTopUpMonth": "bulan", "grokAdditionalCredits": "Kredit Tambahan", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "penebang", "proxyTab": "Proksi", "budgetManagement": "Manajemen Anggaran", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Token Pertama", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 19f45ac1ca..b9c7e6706c 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Garis Waktu Permintaan Visual", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "buka", "close": "tutup" }, - "noResults": "Tidak ada hasil", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Tidak ada hasil" }, "webhooks": { "title": "Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "Pembagian Kuota", "discovery": "Penemuan", "freeProviderRankings": "Peringkat Penyedia Gratis", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Tingkat Gratis", "gamification": "Gamifikasi", "leaderboard": "Papan Peringkat", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Penyedia ini sudah tidak digunakan lagi", "riskNotice": { "title": "Sebelum melanjutkan", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Penyedia dengan peringatan penggunaan — klik untuk detail", "oauth": "Penyedia ini menggunakan sesi produk/OAuth resmi Anda, yang tidak diizinkan untuk penggunaan proksi/router. Kami tidak menyarankan penggunaan agen otonom yang intensif (gaya OpenCloud, alur multi-langkah yang panjang, batch besar) — upstream mungkin bereaksi dengan membatasi atau memblokir akun. Gunakan dengan risiko Anda sendiri.", "webCookie": "Penyedia ini mengautentikasi melalui kuki sesi web Anda. Layanan upstream dapat membatalkan sesi kapan saja, mengharuskan Anda untuk masuk kembali. Tidak disarankan untuk operasi jangka panjang tanpa pengawasan. Gunakan dengan risiko Anda sendiri.", @@ -5107,9 +5111,9 @@ "cancel": "Batal" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "Ambil model upstream secara otomatis", + "autoFetchModelsTooltip": "Ambil dan simpan model upstream saat diperlukan", + "autoFetchModelsEnabled": "Model hulu auto-fetch diaktifkan", + "autoFetchModelsDisabled": "Model upstream auto-fetch dinonaktifkan", + "autoFetchModelsToggleFailed": "Gagal untuk mengubah model upstream auto-fetch", + "autoFetchModelsPartialFailure": "Beberapa koneksi diperbarui, tetapi pengambilan otomatis model upstream tidak berubah di mana-mana", + "overridesUpstreamModel": "Mengganti upstream", + "overridesUpstreamModelHint": "Pengaturan Anda menimpa model upstream ini", + "resetToUpstreamDefaults": "Kembalikan pengaturan default upstream", + "resetToUpstreamDefaultsSuccess": "Mengembalikan pengaturan model upstream ke default", + "resetToUpstreamDefaultsFailed": "Gagal mengembalikan pengaturan model upstream ke default", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Tulis ulang panggilan alat web_fetch bawaan ke /v1/web/fetch milik OmniRoute.", "interceptionLoadError": "Gagal memuat pengaturan intersepsi: {error}", "interceptionSaveError": "Gagal menyimpan pengaturan intersepsi: {error}", - "ccAliasSectionTitle": "Expose di Claude Code (claude/…)", - "ccAliasSectionHint": "Iklankan model penyedia ini di bawah claude/<provider>/<model> ID cermin agar penemuan model gateway Claude Code dapat mencantumkannya. Mati secara default — mengaktifkan ini menggandakan entri katalog untuk semua klien.", - "ccAliasProviderLevelLabel": "Penyedia default", - "ccAliasModelOverridesLabel": "Penggantian per-model", - "ccAliasModelOverrideAriaLabel": "Override untuk {modelId}", - "ccAliasStateInherit": "Warisi", - "ccAliasStateOn": "Hidup", - "ccAliasStateOff": "Matikan", - "ccAliasAddModelPlaceholder": "Model id (mis. gpt-4o)", - "ccAliasAddModelButton": "Tambahkan override", - "ccAliasLoadError": "Gagal memuat pengaturan discovery-alias: {error}", - "ccAliasSaveError": "Gagal menyimpan pengaturan discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Hubungkan Galadriel dengan kunci API.", "predibase": "Kredit uji coba gratis $25 (validitas 30 hari)", "chenzk": "Gateway yang kompatibel dengan OpenAI dengan katalog model langsung di chenzk.top.", - "freepik": "Hasilkan gambar dengan Mystic API dari Freepik.", + "magnific": "Hasilkan gambar dengan Mystic API dari Freepik.", "freetheai": "Gateway gratis yang kompatibel dengan OpenAI dengan dukungan model passthrough.", "g4f-gemini": "Proksi terbalik g4f.space tanpa kunci gratis ke Gemini, dibatasi hingga 5 permintaan per menit.", "g4f-groq": "Proksi terbalik g4f.space tanpa kunci gratis ke Groq, dibatasi hingga 5 permintaan per menit.", @@ -6209,6 +6224,7 @@ "claude": "Hubungkan Claude Code dengan alur OAuth yang ada.", "cline": "Hubungkan Cline dengan alur OAuth yang ada.", "cursor": "Hubungkan Cursor IDE dengan alur OAuth yang ada.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Hubungkan GitHub Copilot dengan alur OAuth yang ada.", "gitlab-duo": "Aplikasi OAuth dengan cakupan ai_features + read_user. Konfigurasikan GITLAB_DUO_OAUTH_CLIENT_ID dan secara opsional GITLAB_DUO_OAUTH_CLIENT_SECRET pada instansi OmniRoute ini.", "kilocode": "Hubungkan Kilo Code dengan alur OAuth yang ada.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Dalam masa tunggu", "codexPoolUsed": "terpakai", "codexPoolUntil": "Hingga {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback anonim", "anonymousFallbackDesc": "Ketika semua koneksi yang dikonfigurasi habis (kuota, kredit, atau masa berlaku), gunakan sementara tingkat tanpa kunci penyedia ini. Matikan untuk melewati penyedia ini alih-alih mengirim permintaan anonim — disarankan ketika tingkat tanpa kunci menolak permintaan tersebut (401).", "anonymousFallbackEnabled": "Fallback anonim diaktifkan untuk {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Pengaturan endpoint model yang disimpan", "searchByModelAria": "Cari berdasarkan model", "selectSupportedEndpoint": "Pilih setidaknya satu endpoint yang didukung", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Ambil model upstream secara otomatis", - "autoFetchModelsDisabled": "Model upstream auto-fetch dinonaktifkan", - "autoFetchModelsEnabled": "Model hulu auto-fetch diaktifkan", - "autoFetchModelsToggleFailed": "Gagal untuk mengubah model upstream auto-fetch", - "overridesUpstreamModel": "Mengganti upstream", - "autoFetchModelsPartialFailure": "Beberapa koneksi diperbarui, tetapi pengambilan otomatis model upstream tidak berubah di mana-mana", - "overridesUpstreamModelHint": "Pengaturan Anda menimpa model upstream ini", - "resetToUpstreamDefaults": "Kembalikan pengaturan default upstream", - "resetToUpstreamDefaultsSuccess": "Mengembalikan pengaturan model upstream ke default", - "resetToUpstreamDefaultsFailed": "Gagal mengembalikan pengaturan model upstream ke default", - "autoFetchModelsTooltip": "Ambil dan simpan model upstream saat diperlukan" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Kata Kunci yang Dilarang", "customBannedSignalsDesc": "Kata kunci tambahan yang memicu deteksi pemblokiran akun permanen. Kata kunci bawaan selalu berlaku.", "customBannedSignalsPlaceholder": "mis. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "dikonfigurasi", "none": "Tidak ada", "modelOverrideValuePlaceholder": "Nilai numerik", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Tambah nilai kunci", "noModelOverrides": "Tidak ada penimpaan yang dikonfigurasi untuk model ini.", "modelOverrideLoadFailed": "Gagal memuat penimpaan model", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Tionghoa Klasik (hanya tersedia untuk bahasa Tionghoa)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Kombo round-robin dan acak beralih ke koneksi yang berbeda pada setiap permintaan, alih-alih menetapkan seluruh percakapan ke satu koneksi berdasarkan hash pesan pertama. Biarkan nonaktif untuk mempertahankan hit prompt-cache pada obrolan multi-putaran. Pengabaian per-kombo memiliki prioritas lebih tinggi.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Redaksi Kredensial", "credentialRedactionDesc": "Redaksikan kunci API, token, dan rahasia dari konteks yang dikirim ke penyedia dan dari respons.", "enableCredentialRedaction": "Aktifkan redaksi kredensial", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Aktifkan mesin", "enableDescription": "Berjalan terakhir dalam tumpukan (setelah RTK/Caveman membersihkan teks, OmniGlyph mengonversi sisanya menjadi gambar) dan juga berjalan mandiri melalui mode omniglyph. Ini adalah pratinjau dan tetap nonaktif secara default hingga validasi menyeluruh selesai.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Tersimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Aktifkan mesin OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksimum", "grokAutoTopUpMonth": "bulan", "grokAdditionalCredits": "Kredit Tambahan", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Token Pertama", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 7332c3206b..d777c72bf1 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Timeline visiva delle richieste", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "apri", "close": "chiudi" }, - "noResults": "Nessun risultato", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Nessun risultato" }, "webhooks": { "title": "Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "Quota condivisa", "discovery": "Scoperta", "freeProviderRankings": "Classifiche provider gratuiti", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Piani gratuiti", "gamification": "Gamification", "leaderboard": "Classifica", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Questo provider è stato deprecato", "riskNotice": { "title": "Prima di continuare", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provider con avvertenze d'uso — fai clic per i dettagli", "oauth": "Questo provider utilizza la sessione/OAuth ufficiale del prodotto, che non è autorizzata per l'uso come proxy/router. Si sconsiglia l'uso intensivo di agenti autonomi (in stile OpenCloud, flussi lunghi a più passaggi, batch di grandi dimensioni): l'upstream potrebbe reagire limitando o bloccando l'account. Utilizzare a proprio rischio.", "webCookie": "Questo provider si autentica tramite i cookie della sessione web. Il servizio upstream potrebbe invalidare la sessione in qualsiasi momento, richiedendo di effettuare nuovamente l'accesso. Non consigliato per operazioni prolungate non presidiate. Utilizzare a proprio rischio.", @@ -5107,9 +5111,9 @@ "cancel": "Annulla" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabilitato", "enableProvider": "Abilita fornitore", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Salto {count} modelli esistenti", "autoSync": "Sincronizzazione automatica", "autoSyncShort": "Sincronizza", + "autoFetchModels": "Recupera automaticamente i modelli upstream", + "autoFetchModelsTooltip": "Recupera e memorizza nella cache i modelli upstream quando necessario", + "autoFetchModelsEnabled": "Modello upstream auto-fetch abilitato", + "autoFetchModelsDisabled": "Fetch automatico del modello upstream disabilitato", + "autoFetchModelsToggleFailed": "Impossibile attivare/disattivare il recupero automatico del modello upstream", + "autoFetchModelsPartialFailure": "Alcune connessioni aggiornate, ma l'auto-fetch del modello upstream non è stato cambiato ovunque", + "overridesUpstreamModel": "Sovrascrive upstream", + "overridesUpstreamModelHint": "Le tue impostazioni sovrascrivono questo modello upstream", + "resetToUpstreamDefaults": "Ripristina le impostazioni predefinite upstream", + "resetToUpstreamDefaultsSuccess": "Ripristinati i valori predefiniti del modello upstream", + "resetToUpstreamDefaultsFailed": "Impossibile ripristinare le impostazioni predefinite del modello upstream", "autoSyncTooltip": "Aggiorna automaticamente l'elenco dei modelli ogni 24 ore (configurabile tramite MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Sincronizzazione automatica abilitata — i modelli verranno aggiornati periodicamente", "autoSyncDisabled": "Sincronizzazione automatica disabilitata", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Riscrivi le chiamate dello strumento nativo web_fetch verso /v1/web/fetch di OmniRoute.", "interceptionLoadError": "Impossibile caricare le impostazioni di intercettazione: {error}", "interceptionSaveError": "Impossibile salvare le impostazioni di intercettazione: {error}", - "ccAliasSectionTitle": "Esponi in Claude Code (claude/…)", - "ccAliasSectionHint": "Mostra i modelli di questo provider sotto gli id specchio claude/<provider>/<model> così la scoperta modelli gateway di Claude Code può elencarli. Disattivato per impostazione predefinita — abilitarlo raddoppia le voci nel catalogo per tutti i client.", - "ccAliasProviderLevelLabel": "Impostazione predefinita del provider", - "ccAliasModelOverridesLabel": "Sostituzioni per modello", - "ccAliasModelOverrideAriaLabel": "Sostituzione per {modelId}", - "ccAliasStateInherit": "Eredita", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", "ccAliasStateOn": "On", "ccAliasStateOff": "Off", - "ccAliasAddModelPlaceholder": "Id modello (es. gpt-4o)", - "ccAliasAddModelButton": "Aggiungi sostituzione", - "ccAliasLoadError": "Impossibile caricare le impostazioni discovery-alias: {error}", - "ccAliasSaveError": "Impossibile salvare l'impostazione discovery-alias: {error}", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Connetti Galadriel con una chiave API.", "predibase": "$25 di crediti di prova gratuiti (validità di 30 giorni)", "chenzk": "Gateway compatibile con OpenAI con un catalogo di modelli live su chenzk.top.", - "freepik": "Genera immagini con la Mystic API di Freepik.", + "magnific": "Genera immagini con la Mystic API di Freepik.", "freetheai": "Gateway gratuito compatibile con OpenAI con supporto per modelli passthrough.", "g4f-gemini": "Reverse proxy gratuito senza chiave di g4f.space verso Gemini, limitato a 5 richieste al minuto.", "g4f-groq": "Reverse proxy gratuito senza chiave di g4f.space verso Groq, limitato a 5 richieste al minuto.", @@ -6209,6 +6224,7 @@ "claude": "Connetti Claude Code con il flusso OAuth esistente.", "cline": "Connetti Cline con il flusso OAuth esistente.", "cursor": "Connetti Cursor IDE con il flusso OAuth esistente.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Connetti GitHub Copilot con il flusso OAuth esistente.", "gitlab-duo": "Applicazione OAuth con scope ai_features + read_user. Configura GITLAB_DUO_OAUTH_CLIENT_ID e opzionalmente GITLAB_DUO_OAUTH_CLIENT_SECRET su questa istanza di OmniRoute.", "kilocode": "Connetti Kilo Code con il flusso OAuth esistente.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "In attesa", "codexPoolUsed": "utilizzato", "codexPoolUntil": "Fino a {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback anonimo", "anonymousFallbackDesc": "Quando tutte le connessioni configurate sono esaurite (quota, crediti o scadenza), utilizza temporaneamente il livello senza chiave di questo fornitore. Disattiva per saltare questo fornitore invece di inviare richieste anonime — consigliato quando il livello senza chiave le rifiuta (401).", "anonymousFallbackEnabled": "Fallback anonimo abilitato per {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Impostazioni dell'endpoint del modello salvato", "searchByModelAria": "Cerca per modello", "selectSupportedEndpoint": "Seleziona almeno un endpoint supportato", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsTooltip": "Recupera e memorizza nella cache i modelli upstream quando necessario", - "autoFetchModels": "Recupera automaticamente i modelli upstream", - "autoFetchModelsDisabled": "Fetch automatico del modello upstream disabilitato", - "autoFetchModelsToggleFailed": "Impossibile attivare/disattivare il recupero automatico del modello upstream", - "overridesUpstreamModel": "Sovrascrive upstream", - "autoFetchModelsPartialFailure": "Alcune connessioni aggiornate, ma l'auto-fetch del modello upstream non è stato cambiato ovunque", - "overridesUpstreamModelHint": "Le tue impostazioni sovrascrivono questo modello upstream", - "resetToUpstreamDefaults": "Ripristina le impostazioni predefinite upstream", - "resetToUpstreamDefaultsSuccess": "Ripristinati i valori predefiniti del modello upstream", - "resetToUpstreamDefaultsFailed": "Impossibile ripristinare le impostazioni predefinite del modello upstream", - "autoFetchModelsEnabled": "Modello upstream auto-fetch abilitato" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Impostazioni", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Parole chiave vietate", "customBannedSignalsDesc": "Parole chiave aggiuntive che attivano il rilevamento del ban permanente dell'account. Le parole chiave integrate si applicano sempre.", "customBannedSignalsPlaceholder": "es. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "configurato", "none": "Nessuno", "modelOverrideValuePlaceholder": "Valore numerico", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Aggiungi chiave-valore", "noModelOverrides": "Nessun override configurato per questo modello.", "modelOverrideLoadFailed": "Impossibile caricare gli override del modello", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK conciso (文言)", "description": "Stile ultra-conciso in cinese classico (disponibile solo per il cinese)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Le combinazioni round-robin e casuali ruotano su una connessione diversa a ogni richiesta invece di associare un'intera conversazione a una sola connessione tramite l'hash del primo messaggio. Lascia disattivato per preservare i riscontri della cache dei prompt per le chat a più turni. Le sostituzioni per singola combinazione hanno la precedenza.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Oscuramento delle credenziali", "credentialRedactionDesc": "Oscura chiavi API, token e segreti dal contesto inviato ai provider e dalle risposte.", "enableCredentialRedaction": "Abilita l'oscuramento delle credenziali", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Abilita il motore", "enableDescription": "Viene eseguito per ultimo nello stack (dopo che RTK/Caveman ha pulito il testo, OmniGlyph converte il resto in immagini) e funziona anche in modalità autonoma tramite la modalità omniglyph. Questa è un'anteprima e rimane disattivata per impostazione predefinita fino al completamento della convalida end-to-end.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Salvato.", "saveFailed": "Impossibile salvare.", "enableAria": "Abilita il motore OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "massimo", "grokAutoTopUpMonth": "mese", "grokAdditionalCredits": "Crediti Aggiuntivi", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Registratore", "proxyTab": "Procura", "budgetManagement": "Gestione del bilancio", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Primo Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 7a12061df8..ee7b56d074 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "ビジュアルリクエストタイムライン", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "開く", "close": "閉じる" }, - "noResults": "結果がありません", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "結果がありません" }, "webhooks": { "title": "Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "クォータ共有", "discovery": "ディスカバリー", "freeProviderRankings": "無料プロバイダーランキング", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "無料枠", "gamification": "ゲーミフィケーション", "leaderboard": "リーダーボード", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "このプロバイダーは廃止されました", "riskNotice": { "title": "続行する前に", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "使用上の注意点があるプロバイダー — クリックして詳細を表示", "oauth": "このプロバイダーは、プロキシ/ルーターでの使用が許可されていない公式製品のセッション/OAuthを使用します。自律型エージェントの集中的な使用(OpenCloudスタイル、長いマルチステップフロー、大量のバッチ処理)は推奨されません。アップストリームがアカウントを制限または禁止する可能性があります。自己責任でご利用ください。", "webCookie": "このプロバイダーは、Webセッションクッキーを使用して認証します。アップストリームサービスはいつでもセッションを無効化する可能性があり、その場合は再ログインが必要になります。長時間の無人運用には推奨されません。自己責任でご利用ください。", @@ -5107,9 +5111,9 @@ "cancel": "キャンセル" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "無効", "enableProvider": "プロバイダーを有効にする", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "{count}件の既存モデルをスキップ", "autoSync": "自動同期", "autoSyncShort": "同期", + "autoFetchModels": "アップストリームモデルを自動取得", + "autoFetchModelsTooltip": "必要に応じてアップストリームモデルを取得してキャッシュする", + "autoFetchModelsEnabled": "上流モデルの自動取得が有効になりました", + "autoFetchModelsDisabled": "上流モデルの自動取得が無効になっています", + "autoFetchModelsToggleFailed": "アップストリームモデルの自動取得の切り替えに失敗しました", + "autoFetchModelsPartialFailure": "いくつかの接続が更新されましたが、上流モデルの自動取得はすべての場所で変更されませんでした", + "overridesUpstreamModel": "上流をオーバーライド", + "overridesUpstreamModelHint": "あなたの設定がこの上流モデルを上書きします", + "resetToUpstreamDefaults": "アップストリームのデフォルトを復元する", + "resetToUpstreamDefaultsSuccess": "アップストリームモデルのデフォルトを復元しました", + "resetToUpstreamDefaultsFailed": "アップストリームモデルのデフォルトを復元できませんでした", "autoSyncTooltip": "24時間ごとにモデルリストを自動更新(MODEL_SYNC_INTERVAL_HOURSで設定可能)", "autoSyncEnabled": "自動同期有効 — モデルは定期的に更新されます", "autoSyncDisabled": "自動同期無効", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "ネイティブの web_fetch ツール呼び出しを OmniRoute の /v1/web/fetch に書き換えます。", "interceptionLoadError": "インターセプト設定の読み込みに失敗しました: {error}", "interceptionSaveError": "インターセプト設定の保存に失敗しました: {error}", - "ccAliasSectionTitle": "Claude Codeで公開する (claude/…)", - "ccAliasSectionHint": "このプロバイダーのモデルを claude/<provider>/<model> ミラー ID の下で広告し、Claude Code のゲートウェイモデル発見がそれらをリストできるようにします。デフォルトではオフになっており、これを有効にするとすべてのクライアントのカタログエントリが2倍になります。", - "ccAliasProviderLevelLabel": "プロバイダーのデフォルト", - "ccAliasModelOverridesLabel": "モデルごとのオーバーライド", - "ccAliasModelOverrideAriaLabel": "{modelId}のオーバーライド", - "ccAliasStateInherit": "継承", - "ccAliasStateOn": "オン", - "ccAliasStateOff": "オフ", - "ccAliasAddModelPlaceholder": "モデルID(例:gpt-4o)", - "ccAliasAddModelButton": "オーバーライドを追加", - "ccAliasLoadError": "ディスカバリーエイリアス設定の読み込みに失敗しました: {error}", - "ccAliasSaveError": "発見エイリアス設定の保存に失敗しました: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "APIキーを使用してGaladrielに接続します。", "predibase": "$25の無料トライアルクレジット(有効期限30日間)", "chenzk": "chenzk.top でライブモデルカタログを提供するOpenAI互換ゲートウェイ。", - "freepik": "FreepikのMystic APIで画像を生成します。", + "magnific": "FreepikのMystic APIで画像を生成します。", "freetheai": "パススルーモデルをサポートする無料のOpenAI互換ゲートウェイ。", "g4f-gemini": "キー不要で無料のg4f.spaceによるGeminiへのリバースプロキシ(1分あたり5リクエストに制限)。", "g4f-groq": "キー不要で無料のg4f.spaceによるGroqへのリバースプロキシ(1分あたり5リクエストに制限)。", @@ -6209,6 +6224,7 @@ "claude": "既存のOAuthフローを使用してClaude Codeに接続します。", "cline": "既存のOAuthフローを使用してClineに接続します。", "cursor": "既存のOAuthフローを使用してCursor IDEに接続します。", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "既存のOAuthフローを使用してGitHub Copilotに接続します。", "gitlab-duo": "ai_features + read_user スコープを持つOAuthアプリケーション。このOmniRouteインスタンスで GITLAB_DUO_OAUTH_CLIENT_ID と、必要に応じて GITLAB_DUO_OAUTH_CLIENT_SECRET を設定してください。", "kilocode": "既存のOAuthフローを使用してKilo Codeに接続します。", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "クールダウン中", "codexPoolUsed": "使用済み", "codexPoolUntil": "{value} まで", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "匿名フォールバック", "anonymousFallbackDesc": "すべての設定された接続が使い果たされた場合(クォータ、クレジット、または有効期限)、このプロバイダーのキーなしティアを一時的に使用します。このプロバイダーをスキップするにはオフにしてください。匿名リクエストを送信する代わりに、キーなしティアがそれらを拒否する場合(401)に推奨されます。", "anonymousFallbackEnabled": "{provider}の匿名フォールバックが有効になりました", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "保存されたモデルエンドポイント設定", "searchByModelAria": "モデルで検索", "selectSupportedEndpoint": "サポートされているエンドポイントを少なくとも1つ選択してください", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsEnabled": "上流モデルの自動取得が有効になりました", - "autoFetchModelsTooltip": "必要に応じてアップストリームモデルを取得してキャッシュする", - "autoFetchModelsDisabled": "上流モデルの自動取得が無効になっています", - "autoFetchModels": "アップストリームモデルを自動取得", - "autoFetchModelsToggleFailed": "アップストリームモデルの自動取得の切り替えに失敗しました", - "overridesUpstreamModel": "上流をオーバーライド", - "autoFetchModelsPartialFailure": "いくつかの接続が更新されましたが、上流モデルの自動取得はすべての場所で変更されませんでした", - "overridesUpstreamModelHint": "あなたの設定がこの上流モデルを上書きします", - "resetToUpstreamDefaults": "アップストリームのデフォルトを復元する", - "resetToUpstreamDefaultsFailed": "アップストリームモデルのデフォルトを復元できませんでした", - "resetToUpstreamDefaultsSuccess": "アップストリームモデルのデフォルトを復元しました" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "設定", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "禁止キーワード", "customBannedSignalsDesc": "アカウントの永久BAN検出のトリガーとなる追加のキーワード。組み込みのキーワードは常に適用されます。", "customBannedSignalsPlaceholder": "例: api key revoked", @@ -7210,6 +7203,7 @@ "configured": "設定済み", "none": "なし", "modelOverrideValuePlaceholder": "数値", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "キーと値を追加", "noModelOverrides": "このモデル用に設定されたオーバーライドはありません。", "modelOverrideLoadFailed": "モデルのオーバーライドの読み込みに失敗しました", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "簡潔なCJK (文言)", "description": "漢文の超簡潔スタイル (中国語でのみ利用可能)。" @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "ラウンドロビンおよびランダムのコンボにおいて、最初のメッセージのハッシュによって会話全体を1つの接続に固定するのではなく、リクエストごとに異なる接続にローテーションします。複数ターンのチャットでプロンプトキャッシュのヒット率を維持するには、オフのままにしてください。コンボごとの個別設定が優先されます。", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "認証情報の秘匿化", "credentialRedactionDesc": "プロバイダーに送信されるコンテキストおよびレスポンスから、API キー、トークン、シークレットを秘匿化します。", "enableCredentialRedaction": "認証情報の秘匿化を有効にする", @@ -8621,6 +8623,27 @@ }, "enableTitle": "エンジンを有効にする", "enableDescription": "スタックの最後に実行され(RTK/Cavemanがテキストをクリーンアップした後、OmniGlyphが残りを画像に変換)、omniglyph モードを介してスタンドアロンでも実行されます。これはプレビュー版であり、エンドツーエンドの検証が完了するまではデフォルトでオフのままになります。", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "保存されました。", "saveFailed": "保存できませんでした。", "enableAria": "OmniGlyphエンジンを有効にする", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "最大", "grokAutoTopUpMonth": "月", "grokAdditionalCredits": "追加のクレジット", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "ロガー", "proxyTab": "プロキシ", "budgetManagement": "予算管理", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "最初のトークン", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "オファー", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days} 日間" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index a003916a18..3a5feebf4e 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "비주얼 요청 타임라인", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "열기", "close": "닫기" }, - "noResults": "결과가 없습니다", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "결과가 없습니다" }, "webhooks": { "title": "웹훅", @@ -1739,8 +1739,8 @@ "quotaShare": "할당량 공유", "discovery": "탐색", "freeProviderRankings": "무료 제공업체 순위", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "무료 티어", "gamification": "게이미피케이션", "leaderboard": "리더보드", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "모델 전반에 요청이 분산되는 방식을 선택하세요 - 14가지 전략 사용 가능", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "이 공급자는 더 이상 사용되지 않습니다.", "riskNotice": { "title": "계속하기 전에", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "사용 시 주의 사항이 있는 제공자 — 자세한 내용을 보려면 클릭하세요", "oauth": "이 제공자는 공식 제품 세션/OAuth를 사용하며, 이는 프록시/라우터 사용에 대해 승인되지 않았습니다. 집중적인 자율 에이전트 사용(OpenCloud 스타일, 긴 다단계 흐름, 대량 배치)은 권장하지 않습니다. 업스트림에서 계정을 제한하거나 차단할 수 있습니다. 본인 책임 하에 사용하십시오.", "webCookie": "이 제공자는 웹 세션 쿠키를 통해 인증합니다. 업스트림 서비스가 언제든지 세션을 무효화할 수 있어 다시 로그인해야 할 수 있습니다. 장시간 자리를 비우는 작업에는 권장하지 않습니다. 본인 책임 하에 사용하십시오.", @@ -5107,9 +5111,9 @@ "cancel": "취소" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "비활성화됨", "enableProvider": "공급자 활성화", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "{count}개의 기존 모델 건너뛰기", "autoSync": "자동 동기화", "autoSyncShort": "동기화", + "autoFetchModels": "업스트림 모델 자동 가져오기", + "autoFetchModelsTooltip": "필요할 때 업스트림 모델을 가져와 캐시합니다.", + "autoFetchModelsEnabled": "업스트림 모델 자동 가져오기 활성화됨", + "autoFetchModelsDisabled": "업스트림 모델 자동 가져오기 비활성화됨", + "autoFetchModelsToggleFailed": "업스트림 모델 자동 가져오기를 전환하지 못했습니다.", + "autoFetchModelsPartialFailure": "일부 연결이 업데이트되었지만, 업스트림 모델 자동 가져오기가 모든 곳에서 변경되지 않았습니다.", + "overridesUpstreamModel": "업스트림 재정의", + "overridesUpstreamModelHint": "귀하의 설정이 이 업스트림 모델을 덮어씁니다.", + "resetToUpstreamDefaults": "업스트림 기본값 복원", + "resetToUpstreamDefaultsSuccess": "복원된 업스트림 모델 기본값", + "resetToUpstreamDefaultsFailed": "업스트림 모델 기본값을 복원하지 못했습니다.", "autoSyncTooltip": "24시간마다 모델 목록 자동 업데이트 (MODEL_SYNC_INTERVAL_HOURS로 구성 가능)", "autoSyncEnabled": "자동 동기화 활성화 — 모델이 주기적으로 업데이트됩니다", "autoSyncDisabled": "자동 동기화 비활성화", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "네이티브 web_fetch 도구 호출을 OmniRoute의 /v1/web/fetch로 재작성합니다.", "interceptionLoadError": "가로채기 설정을 불러오지 못했습니다: {error}", "interceptionSaveError": "가로채기 설정을 저장하지 못했습니다: {error}", - "ccAliasSectionTitle": "Claude 코드에서 노출하기 (claude/…)", - "ccAliasSectionHint": "이 공급자의 모델을 claude/<provider>/<model> 미러 ID 아래에 광고하여 Claude Code의 게이트웨이 모델 검색이 이를 나열할 수 있도록 합니다. 기본적으로 비활성화되어 있으며, 이를 활성화하면 모든 클라이언트에 대해 카탈로그 항목이 두 배로 증가합니다.", - "ccAliasProviderLevelLabel": "제공자 기본값", - "ccAliasModelOverridesLabel": "모델별 재정의", - "ccAliasModelOverrideAriaLabel": "{modelId}에 대한 재정의", - "ccAliasStateInherit": "상속", - "ccAliasStateOn": "켜짐", - "ccAliasStateOff": "꺼짐", - "ccAliasAddModelPlaceholder": "모델 ID (예: gpt-4o)", - "ccAliasAddModelButton": "오버라이드 추가", - "ccAliasLoadError": "discovery-alias 설정을 로드하지 못했습니다: {error}", - "ccAliasSaveError": "discovery-alias 설정을 저장하지 못했습니다: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "API 키로 Galadriel을 연결합니다.", "predibase": "$25 무료 체험 크레딧 (30일 유효)", "chenzk": "chenzk.top의 실시간 모델 카탈로그를 지원하는 OpenAI 호환 게이트웨이입니다.", - "freepik": "Freepik의 Mystic API로 이미지를 생성합니다.", + "magnific": "Freepik의 Mystic API로 이미지를 생성합니다.", "freetheai": "패스스루 모델을 지원하는 무료 OpenAI 호환 게이트웨이입니다.", "g4f-gemini": "키가 필요 없는 무료 g4f.space Gemini 리버스 프록시(분당 5회 요청으로 제한).", "g4f-groq": "키가 필요 없는 무료 g4f.space Groq 리버스 프록시(분당 5회 요청으로 제한).", @@ -6209,6 +6224,7 @@ "claude": "기존 OAuth 흐름으로 Claude Code를 연결합니다.", "cline": "기존 OAuth 흐름으로 Cline을 연결합니다.", "cursor": "기존 OAuth 흐름으로 Cursor IDE를 연결합니다.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "기존 OAuth 흐름으로 GitHub Copilot을 연결합니다.", "gitlab-duo": "ai_features + read_user 스코프가 있는 OAuth 애플리케이션입니다. 이 OmniRoute 인스턴스에서 GITLAB_DUO_OAUTH_CLIENT_ID 및 선택적으로 GITLAB_DUO_OAUTH_CLIENT_SECRET을 구성하세요.", "kilocode": "기존 OAuth 흐름으로 Kilo Code를 연결합니다.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "대기 시간 적용 중", "codexPoolUsed": "사용됨", "codexPoolUntil": "{value}까지", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "익명 대체", "anonymousFallbackDesc": "모든 구성된 연결이 소진되면(쿼터, 크레딧 또는 만료), 이 공급자의 키 없는 계층을 임시로 사용합니다. 익명 요청을 보내는 대신 이 공급자를 건너뛰려면 끄세요. 키 없는 계층이 요청을 거부할 때(401) 권장됩니다.", "anonymousFallbackEnabled": "{provider}에 대한 익명 대체가 활성화되었습니다.", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "저장된 모델 엔드포인트 설정", "searchByModelAria": "모델로 검색", "selectSupportedEndpoint": "지원되는 엔드포인트를 최소한 하나 선택하세요.", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "업스트림 모델 자동 가져오기", - "autoFetchModelsEnabled": "업스트림 모델 자동 가져오기 활성화됨", - "autoFetchModelsDisabled": "업스트림 모델 자동 가져오기 비활성화됨", - "autoFetchModelsTooltip": "필요할 때 업스트림 모델을 가져와 캐시합니다.", - "overridesUpstreamModel": "업스트림 재정의", - "autoFetchModelsPartialFailure": "일부 연결이 업데이트되었지만, 업스트림 모델 자동 가져오기가 모든 곳에서 변경되지 않았습니다.", - "autoFetchModelsToggleFailed": "업스트림 모델 자동 가져오기를 전환하지 못했습니다.", - "overridesUpstreamModelHint": "귀하의 설정이 이 업스트림 모델을 덮어씁니다.", - "resetToUpstreamDefaultsSuccess": "복원된 업스트림 모델 기본값", - "resetToUpstreamDefaults": "업스트림 기본값 복원", - "resetToUpstreamDefaultsFailed": "업스트림 모델 기본값을 복원하지 못했습니다." + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "설정", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "차단 키워드", "customBannedSignalsDesc": "영구 계정 차단 감지를 트리거하는 추가 키워드입니다. 기본 제공 키워드는 항상 적용됩니다.", "customBannedSignalsPlaceholder": "예: api key revoked", @@ -7210,6 +7203,7 @@ "configured": "configured", "none": "None", "modelOverrideValuePlaceholder": "Numeric value", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Add key value", "noModelOverrides": "No overrides configured for this model.", "modelOverrideLoadFailed": "Failed to load model overrides", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "간결한 CJK (文言)", "description": "한문 초간결 스타일 (중국어만 지원)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "라운드 로빈 및 랜덤 콤보는 첫 번째 메시지 해시를 통해 전체 대화를 하나의 연결에 고정하는 대신 요청마다 다른 연결로 순환합니다. 멀티턴 대화에서 프롬프트 캐시 히트를 유지하려면 꺼두세요. 콤보별 재정의가 우선 적용됩니다.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "자격 증명 마스킹", "credentialRedactionDesc": "공급자에게 전송되는 컨텍스트 및 응답에서 API 키, 토큰, 비밀 정보를 마스킹합니다.", "enableCredentialRedaction": "자격 증명 마스킹 활성화", @@ -8621,6 +8623,27 @@ }, "enableTitle": "엔진 활성화", "enableDescription": "스택의 마지막에 실행되며(RTK/Caveman이 텍스트를 정리한 후 OmniGlyph가 나머지를 이미지로 변환), omniglyph 모드를 통해 독립 실행형으로도 실행됩니다. 이것은 미리보기이며 엔드투엔드 검증이 완료될 때까지 기본적으로 비활성화되어 있습니다.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "저장되었습니다.", "saveFailed": "저장할 수 없습니다.", "enableAria": "OmniGlyph 엔진 활성화", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "최대", "grokAutoTopUpMonth": "월", "grokAdditionalCredits": "추가 크레딧", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "로거", "proxyTab": "프록시", "budgetManagement": "예산 관리", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "첫 토큰", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index d7d3d22e29..63afee6a7f 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "दृश्य विनंती कालरेषा", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "उघडा", "close": "बंद करा" }, - "noResults": "कोणतेही परिणाम नाहीत", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "कोणतेही परिणाम नाहीत" }, "webhooks": { "title": "वेबहुक", @@ -1739,8 +1739,8 @@ "quotaShare": "कोटा वाटा", "discovery": "शोध", "freeProviderRankings": "मोफत प्रदाता क्रमवारी", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "मोफत स्तर", "gamification": "गेमिफिकेशन", "leaderboard": "लीडरबोर्ड", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "हा प्रदाता बहिष्कृत केला गेला आहे", "riskNotice": { "title": "पुढे जाण्यापूर्वी", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "वापराच्या मर्यादा असलेला प्रदाता — तपशीलांसाठी क्लिक करा", "oauth": "हा प्रदाता तुमचे अधिकृत उत्पादन सत्र/OAuth वापरतो, जे प्रॉक्सी/राऊटर वापरासाठी अधिकृत नाही. आम्ही सघन स्वायत्त एजंट वापराची (OpenCloud-शैली, लांब बहु-चरण प्रवाह, मोठे बॅचेस) शिफारस करत नाही — अपस्ट्रीम खाते प्रतिबंधित किंवा बॅन करून प्रतिक्रिया देऊ शकते. स्वतःच्या जोखमीवर वापरा.", "webCookie": "हा प्रदाता तुमच्या वेब सत्र कुकीजद्वारे प्रमाणीकरण करतो. अपस्ट्रीम सेवा कोणत्याही वेळी सत्र अवैध करू शकते, ज्यामुळे तुम्हाला पुन्हा लॉग इन करावे लागेल. दीर्घकाळ लक्ष न ठेवलेल्या ऑपरेशन्ससाठी शिफारस केलेली नाही. स्वतःच्या जोखमीवर वापरा.", @@ -5107,9 +5111,9 @@ "cancel": "रद्द करा" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "ऑटो-फेच अपस्ट्रीम मॉडेल्स", + "autoFetchModelsTooltip": "आवश्यकतेनुसार अपस्ट्रीम मॉडेल्स आणा आणि कॅश करा", + "autoFetchModelsEnabled": "उपधारा मॉडेल स्वयंचलित-आकर्षण सक्षम आहे", + "autoFetchModelsDisabled": "उपधारा मॉडेल स्वयंचलित-आकर्षण अक्षम आहे", + "autoFetchModelsToggleFailed": "उपस्ट्रीम मॉडेल ऑटो-फेच टॉगल करण्यात अयशस्वी", + "autoFetchModelsPartialFailure": "काही कनेक्शन अद्यतनित झाले, परंतु अपस्ट्रीम मॉडेल ऑटो-फेच सर्वत्र बदलले नाही.", + "overridesUpstreamModel": "उपधारक ओव्हरराइड्स", + "overridesUpstreamModelHint": "तुमच्या सेटिंग्ज या अपस्ट्रीम मॉडेलला ओव्हरराईड करतात", + "resetToUpstreamDefaults": "अपस्ट्रीम डिफॉल्ट्स पुनर्स्थापित करा", + "resetToUpstreamDefaultsSuccess": "उपस्ट्रीम मॉडेल डिफॉल्ट्स पुनर्स्थापित केले", + "resetToUpstreamDefaultsFailed": "उपस्ट्रीम मॉडेल डिफॉल्ट्स पुनर्स्थापित करण्यात अयशस्वी", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "मूळ web_fetch टूल कॉल्स OmniRoute च्या /v1/web/fetch वर पुन्हा लिहा.", "interceptionLoadError": "इंटरसेप्शन सेटिंग्ज लोड करण्यात अयशस्वी: {error}", "interceptionSaveError": "इंटरसेप्शन सेटिंग्ज सेव्ह करण्यात अयशस्वी: {error}", - "ccAliasSectionTitle": "Claude कोडमध्ये उघडा (claude/…)", - "ccAliasSectionHint": "या प्रदात्याच्या मॉडेल्सची जाहिरात claude/<provider>/<model> मिरर आयडी अंतर्गत करा जेणेकरून Claude Code च्या गेटवे मॉडेल शोधाने त्यांची यादी करू शकेल. डीफॉल्टने बंद — हे सक्षम केल्याने सर्व क्लायंटसाठी कॅटलॉग नोंदी दुप्पट होतात.", - "ccAliasProviderLevelLabel": "प्रदाता डिफॉल्ट", - "ccAliasModelOverridesLabel": "प्रत्येक मॉडेलसाठी ओव्हरराइड्स", - "ccAliasModelOverrideAriaLabel": "{modelId} साठी ओव्हरराइड", - "ccAliasStateInherit": "विरासत", - "ccAliasStateOn": "वर", - "ccAliasStateOff": "बंद", - "ccAliasAddModelPlaceholder": "मॉडेल आयडी (उदा. gpt-4o)", - "ccAliasAddModelButton": "ओव्हरराइड जोडा", - "ccAliasLoadError": "डिस्कवरी-अलियास सेटिंग्ज लोड करण्यात अयशस्वी: {error}", - "ccAliasSaveError": "डिस्कवरी-उपनाम सेटिंग जतन करण्यात अयशस्वी: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "API की सह Galadriel कनेक्ट करा.", "predibase": "$25 मोफत ट्रायल क्रेडिट्स (30 दिवसांची वैधता)", "chenzk": "chenzk.top वर थेट मॉडेल कॅटलॉगसह OpenAI-सुसंगत गेटवे.", - "freepik": "Freepik च्या Mystic API सह प्रतिमा तयार करा.", + "magnific": "Freepik च्या Mystic API सह प्रतिमा तयार करा.", "freetheai": "passthrough मॉडेल समर्थनासह मोफत OpenAI-सुसंगत गेटवे.", "g4f-gemini": "Gemini साठी मोफत विना-की g4f.space रिव्हर्स प्रॉक्सी, प्रति मिनिट 5 विनंत्यांपर्यंत मर्यादित.", "g4f-groq": "Groq साठी मोफत विना-की g4f.space रिव्हर्स प्रॉक्सी, प्रति मिनिट 5 विनंत्यांपर्यंत मर्यादित.", @@ -6209,6 +6224,7 @@ "claude": "सध्याच्या OAuth फ्लोसह Claude Code कनेक्ट करा.", "cline": "सध्याच्या OAuth फ्लोसह Cline कनेक्ट करा.", "cursor": "सध्याच्या OAuth फ्लोसह Cursor IDE कनेक्ट करा.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "सध्याच्या OAuth फ्लोसह GitHub Copilot कनेक्ट करा.", "gitlab-duo": "ai_features + read_user स्कोप्ससह OAuth ॲप्लिकेशन. या OmniRoute इन्स्टन्सवर GITLAB_DUO_OAUTH_CLIENT_ID आणि पर्यायीपणे GITLAB_DUO_OAUTH_CLIENT_SECRET कॉन्फिगर करा.", "kilocode": "सध्याच्या OAuth फ्लोसह Kilo Code कनेक्ट करा.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "प्रतीक्षा कालावधीत", "codexPoolUsed": "वापरले", "codexPoolUntil": "{value} पर्यंत", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "अज्ञात बॅकअप", "anonymousFallbackDesc": "जेव्हा सर्व कॉन्फिगर केलेले कनेक्शन संपतात (कोटा, क्रेडिट्स, किंवा कालावधी), तेव्हा तात्पुरते या प्रदात्याचा कीलेस स्तर वापरा. गुप्त विनंत्या पाठविण्याऐवजी या प्रदात्याला वगळण्यासाठी बंद करा — जेव्हा कीलेस स्तर त्यांना नकार देतो (401) तेव्हा शिफारस केले जाते.", "anonymousFallbackEnabled": "{provider} साठी गुप्तFallback सक्षम आहे", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "सुरक्षित केलेल्या मॉडेल एंडपॉइंट सेटिंग्ज", "searchByModelAria": "मॉडेलद्वारे शोधा", "selectSupportedEndpoint": "किमान एक समर्थित एंडपॉइंट निवडा", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "ऑटो-फेच अपस्ट्रीम मॉडेल्स", - "autoFetchModelsDisabled": "उपधारा मॉडेल स्वयंचलित-आकर्षण अक्षम आहे", - "autoFetchModelsTooltip": "आवश्यकतेनुसार अपस्ट्रीम मॉडेल्स आणा आणि कॅश करा", - "autoFetchModelsEnabled": "उपधारा मॉडेल स्वयंचलित-आकर्षण सक्षम आहे", - "autoFetchModelsToggleFailed": "उपस्ट्रीम मॉडेल ऑटो-फेच टॉगल करण्यात अयशस्वी", - "overridesUpstreamModel": "उपधारक ओव्हरराइड्स", - "autoFetchModelsPartialFailure": "काही कनेक्शन अद्यतनित झाले, परंतु अपस्ट्रीम मॉडेल ऑटो-फेच सर्वत्र बदलले नाही.", - "overridesUpstreamModelHint": "तुमच्या सेटिंग्ज या अपस्ट्रीम मॉडेलला ओव्हरराईड करतात", - "resetToUpstreamDefaultsSuccess": "उपस्ट्रीम मॉडेल डिफॉल्ट्स पुनर्स्थापित केले", - "resetToUpstreamDefaults": "अपस्ट्रीम डिफॉल्ट्स पुनर्स्थापित करा", - "resetToUpstreamDefaultsFailed": "उपस्ट्रीम मॉडेल डिफॉल्ट्स पुनर्स्थापित करण्यात अयशस्वी" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "प्रतिबंधित कीवर्ड", "customBannedSignalsDesc": "अतिरिक्त कीवर्ड जे कायमचे खाते बंदी शोधणे ट्रिगर करतात. अंगभूत कीवर्ड नेहमी लागू होतात.", "customBannedSignalsPlaceholder": "उदा. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "कॉन्फिगर केलेले", "none": "काहीही नाही", "modelOverrideValuePlaceholder": "संख्यात्मक मूल्य", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "की व्हॅल्यू जोडा", "noModelOverrides": "या मॉडेलसाठी कोणतेही ओव्हरराइड्स कॉन्फिगर केलेले नाहीत.", "modelOverrideLoadFailed": "मॉडेल ओव्हरराइड्स लोड करण्यात अयशस्वी", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "संक्षिप्त CJK (文言)", "description": "अभिजात-चिनी अति-संक्षिप्त शैली (केवळ चिनी भाषेसाठी उपलब्ध)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "राउंड-रॉबिन आणि रँडम कॉम्बोज पहिल्या-मेसेज हॅशद्वारे संपूर्ण संभाषण एका कनेक्शनवर पिन करण्याऐवजी प्रत्येक विनंतीवर वेगळ्या कनेक्शनवर रोटेट होतात. मल्टी-टर्न चॅट्ससाठी प्रॉम्प्ट-कॅशे हिट्स जतन करण्यासाठी हे बंद ठेवा. प्रति-कॉम्बो ओव्हरराइड्सना प्राधान्य दिले जाईल.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "क्रेडेन्शियल रिडॅक्शन", "credentialRedactionDesc": "प्रदात्यांना पाठवलेल्या संदर्भातून आणि प्रतिसादांमधून API की, टोकन आणि सिक्रेट्स रिडॅक्ट करा.", "enableCredentialRedaction": "क्रेडेंशियल रिडॅक्शन सक्षम करा", @@ -8621,6 +8623,27 @@ }, "enableTitle": "इंजिन सक्षम करा", "enableDescription": "स्टॅकमध्ये शेवटी चालते (RTK/Caveman मजकूर साफ केल्यानंतर, OmniGlyph उर्वरित मजकूर इमेजेसमध्ये रूपांतरित करते) आणि omniglyph मोडद्वारे स्वतंत्रपणे देखील चालते. हे एक पूर्वावलोकन आहे आणि एंड-टू-एंड प्रमाणीकरण पूर्ण होईपर्यंत डीफॉल्टनुसार बंद राहते.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "जतन केले.", "saveFailed": "जतन करता आले नाही.", "enableAria": "OmniGlyph इंजिन सक्षम करा", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "कमाल", "grokAutoTopUpMonth": "महिना", "grokAdditionalCredits": "अतिरिक्त श्रेय", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "पहिला टोकन", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 3f7482ff8f..b6c322c3e2 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Garis Masa Permintaan Visual", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "buka", "close": "tutup" }, - "noResults": "Tiada hasil", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Tiada hasil" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Perkongsian Kuota", "discovery": "Penemuan", "freeProviderRankings": "Kedudukan Penyedia Percuma", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Tier Percuma", "gamification": "Gamifikasi", "leaderboard": "Papan Pendahulu", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Pembekal ini telah ditamatkan", "riskNotice": { "title": "Sebelum meneruskan", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Penyedia dengan kekangan penggunaan — klik untuk butiran", "oauth": "Penyedia ini menggunakan sesi produk/OAuth rasmi anda, yang tidak dibenarkan untuk penggunaan proksi/penghala. Kami tidak mengesyorkan penggunaan ejen autonomi yang intensif (gaya OpenCloud, aliran berbilang langkah yang panjang, kelompok besar) — upstream mungkin bertindak balas dengan menyekat atau mengharamkan akaun tersebut. Gunakan atas risiko anda sendiri.", "webCookie": "Penyedia ini mengesahkan melalui kuki sesi web anda. Perkhidmatan upstream mungkin membatalkan sesi pada bila-bila masa, memerlukan anda untuk log masuk semula. Tidak disyorkan untuk operasi tanpa pengawasan yang lama. Gunakan atas risiko anda sendiri.", @@ -5107,9 +5111,9 @@ "cancel": "Batal" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Dilumpuhkan", "enableProvider": "Dayakan pembekal", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Melangkau {count} model sedia ada", "autoSync": "Auto-Segerak", "autoSyncShort": "Segerak", + "autoFetchModels": "Ambil model hulu secara automatik", + "autoFetchModelsTooltip": "Ambil dan simpan model hulu apabila diperlukan", + "autoFetchModelsEnabled": "Model hulu auto-fetch diaktifkan", + "autoFetchModelsDisabled": "Model hulu auto-fetch dinyahdayakan", + "autoFetchModelsToggleFailed": "Gagal untuk menghidupkan model upstream auto-fetch", + "autoFetchModelsPartialFailure": "Beberapa sambungan telah dikemas kini, tetapi pengambilan auto model hulu tidak diubah di semua tempat", + "overridesUpstreamModel": "Mengganti hulu", + "overridesUpstreamModelHint": "Tetapan anda mengatasi model hulu ini", + "resetToUpstreamDefaults": "Pulihkan tetapan asal upstream", + "resetToUpstreamDefaultsSuccess": "Mengembalikan tetapan lalai model upstream", + "resetToUpstreamDefaultsFailed": "Gagal untuk memulihkan tetapan lalai model upstream", "autoSyncTooltip": "Muat semula senarai model secara automatik setiap 24j (boleh dikonfigurasikan melalui MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Autosegerak didayakan — model akan dimuat semula secara berkala", "autoSyncDisabled": "Autosegerak dilumpuhkan", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Tulis semula panggilan alat web_fetch asli ke /v1/web/fetch OmniRoute.", "interceptionLoadError": "Gagal memuatkan tetapan pintasan: {error}", "interceptionSaveError": "Gagal menyimpan tetapan pintasan: {error}", - "ccAliasSectionTitle": "Dedahkan dalam Claude Code (claude/…)", - "ccAliasSectionHint": "Iklankan model penyedia ini di bawah claude/<provider>/<model> ID cermin supaya penemuan model gateway Claude Code dapat menyenaraikannya. Dimatikan secara lalai — mengaktifkannya menggandakan entri katalog untuk semua pelanggan.", - "ccAliasProviderLevelLabel": "Penyedia lalai", - "ccAliasModelOverridesLabel": "Tindakan mengikut model", - "ccAliasModelOverrideAriaLabel": "Tindakan Ganti untuk {modelId}", - "ccAliasStateInherit": "Warisi", - "ccAliasStateOn": "Hidup", - "ccAliasStateOff": "Matikan", - "ccAliasAddModelPlaceholder": "Id Model (contoh: gpt-4o)", - "ccAliasAddModelButton": "Tambah override", - "ccAliasLoadError": "Gagal memuat tetapan discovery-alias: {error}", - "ccAliasSaveError": "Gagal menyimpan tetapan discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Sambungkan Galadriel dengan kunci API.", "predibase": "Kredit percubaan percuma $25 (tempoh sah 30 hari)", "chenzk": "Gerbang serasi OpenAI dengan katalog model langsung di chenzk.top.", - "freepik": "Jana imej dengan API Mystic Freepik.", + "magnific": "Jana imej dengan API Mystic Freepik.", "freetheai": "Gerbang serasi OpenAI percuma dengan sokongan model passthrough.", "g4f-gemini": "Proksi terbalik g4f.space tanpa kunci percuma ke Gemini, terhad kepada 5 permintaan seminit.", "g4f-groq": "Proksi terbalik g4f.space tanpa kunci percuma ke Groq, terhad kepada 5 permintaan seminit.", @@ -6209,6 +6224,7 @@ "claude": "Sambungkan Claude Code dengan aliran OAuth sedia ada.", "cline": "Sambungkan Cline dengan aliran OAuth sedia ada.", "cursor": "Sambungkan Cursor IDE dengan aliran OAuth sedia ada.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Sambungkan GitHub Copilot dengan aliran OAuth sedia ada.", "gitlab-duo": "Aplikasi OAuth dengan skop ai_features + read_user. Konfigurasikan GITLAB_DUO_OAUTH_CLIENT_ID dan secara pilihan GITLAB_DUO_OAUTH_CLIENT_SECRET pada tika OmniRoute ini.", "kilocode": "Sambungkan Kilo Code dengan aliran OAuth sedia ada.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Dalam tempoh menunggu", "codexPoolUsed": "digunakan", "codexPoolUntil": "Sehingga {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback Tanpa Nama", "anonymousFallbackDesc": "Apabila semua sambungan yang dikonfigurasikan habis (kuota, kredit, atau tamat tempoh), gunakan sementara tier tanpa kunci penyedia ini. Matikan untuk mengabaikan penyedia ini daripada menghantar permintaan tanpa nama — disyorkan apabila tier tanpa kunci menolak permintaan tersebut (401).", "anonymousFallbackEnabled": "Fallback tanpa nama diaktifkan untuk {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Tetapan titik akhir model yang disimpan", "searchByModelAria": "Cari mengikut model", "selectSupportedEndpoint": "Pilih sekurang-kurangnya satu titik akhir yang disokong", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsEnabled": "Model hulu auto-fetch diaktifkan", - "autoFetchModels": "Ambil model hulu secara automatik", - "autoFetchModelsTooltip": "Ambil dan simpan model hulu apabila diperlukan", - "autoFetchModelsToggleFailed": "Gagal untuk menghidupkan model upstream auto-fetch", - "autoFetchModelsDisabled": "Model hulu auto-fetch dinyahdayakan", - "overridesUpstreamModel": "Mengganti hulu", - "autoFetchModelsPartialFailure": "Beberapa sambungan telah dikemas kini, tetapi pengambilan auto model hulu tidak diubah di semua tempat", - "overridesUpstreamModelHint": "Tetapan anda mengatasi model hulu ini", - "resetToUpstreamDefaults": "Pulihkan tetapan asal upstream", - "resetToUpstreamDefaultsFailed": "Gagal untuk memulihkan tetapan lalai model upstream", - "resetToUpstreamDefaultsSuccess": "Mengembalikan tetapan lalai model upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "tetapan", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Kata Kunci Disekat", "customBannedSignalsDesc": "Kata kunci tambahan yang mencetuskan pengesanan sekatan akaun kekal. Kata kunci terbina dalam sentiasa digunakan.", "customBannedSignalsPlaceholder": "cth. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "dikonfigurasikan", "none": "Tiada", "modelOverrideValuePlaceholder": "Nilai angka", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Tambah nilai kunci", "noModelOverrides": "Tiada pintasan dikonfigurasikan untuk model ini.", "modelOverrideLoadFailed": "Gagal memuatkan pintasan model", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK Ringkas (文言)", "description": "Gaya ultra-ringkas Bahasa Cina Klasik (hanya tersedia untuk bahasa Cina)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Kombinasi round-robin dan rawak bertukar ke sambungan yang berbeza pada setiap permintaan dan bukannya menyematkan keseluruhan perbualan pada satu sambungan melalui hash mesej pertama. Biarkan dimatikan untuk mengekalkan hit cache prompt bagi sembang berbilang giliran. Penggantian bagi setiap kombinasi diutamakan.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Redaksi Kredensial", "credentialRedactionDesc": "Redaksikan kunci API, token, dan rahsia daripada konteks yang dihantar kepada penyedia dan daripada respons.", "enableCredentialRedaction": "Dayakan penyuntingan kelayakan", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Dayakan enjin", "enableDescription": "Berjalan terakhir dalam tindanan (selepas RTK/Caveman membersihkan teks, OmniGlyph menukar baki kepada imej) dan juga berjalan secara bersendirian melalui mod omniglyph. Ini ialah pratonton dan kekal dimatikan secara lalai sehingga pengesahan hujung-ke-hujung selesai.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Disimpan.", "saveFailed": "Tidak dapat menyimpan.", "enableAria": "Dayakan enjin OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksimum", "grokAutoTopUpMonth": "bulan", "grokAdditionalCredits": "Kredit Tambahan", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Pembalak", "proxyTab": "proksi", "budgetManagement": "Pengurusan Belanjawan", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Token Pertama", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 5355f29c17..397c206888 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visueel verzoek tijdlijn", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "open", "close": "sluiten" }, - "noResults": "Geen resultaten", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Geen resultaten" }, "webhooks": { "title": "Webhaken", @@ -1739,8 +1739,8 @@ "quotaShare": "Quota-aandeel", "discovery": "Ontdekking", "freeProviderRankings": "Ranglijst gratis providers", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Gratis niveaus", "gamification": "Gamification", "leaderboard": "Leaderboard", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Deze aanbieder is beëindigd", "riskNotice": { "title": "Voordat je doorgaat", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provider met gebruiksvoorbehouden — klik voor details", "oauth": "Deze provider gebruikt je officiële productsessie/OAuth, die niet is geautoriseerd voor proxy-/routergebruik. We raden intensief gebruik van autonome agenten (OpenCloud-stijl, lange stappenreeksen, grote batches) af — de upstream kan reageren door het account te beperken of te blokkeren. Gebruik op eigen risico.", "webCookie": "Deze provider authenticeert via je websessiecookies. De upstream-dienst kan de sessie op elk moment ongeldig maken, waardoor je opnieuw moet inloggen. Niet aanbevolen voor langdurig onbeheerd gebruik. Gebruik op eigen risico.", @@ -5107,9 +5111,9 @@ "cancel": "Annuleren" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Uitgeschakeld", "enableProvider": "Aanbieder inschakelen", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "{count} bestaande modellen overgeslagen", "autoSync": "Automatische synchronisatie", "autoSyncShort": "Synchroniseren", + "autoFetchModels": "Automatisch upstream-modellen ophalen", + "autoFetchModelsTooltip": "Haal upstream-modellen op en cache ze indien nodig", + "autoFetchModelsEnabled": "Upstream model auto-fetch ingeschakeld", + "autoFetchModelsDisabled": "Auto-fetch van upstreammodel uitgeschakeld", + "autoFetchModelsToggleFailed": "Kon upstream model auto-fetch niet omzetten", + "autoFetchModelsPartialFailure": "Sommige verbindingen zijn bijgewerkt, maar het automatisch ophalen van het upstream-model is niet overal gewijzigd", + "overridesUpstreamModel": "Overschrijft upstream", + "overridesUpstreamModelHint": "Jouw instellingen overschrijven dit upstream model", + "resetToUpstreamDefaults": "Herstel upstream standaardinstellingen", + "resetToUpstreamDefaultsSuccess": "Herstelde standaardinstellingen van upstream-model", + "resetToUpstreamDefaultsFailed": "Het is niet gelukt om de standaardinstellingen van het upstream-model te herstellen", "autoSyncTooltip": "Modellijst automatisch elke 24 uur vernieuwen (configureerbaar via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatische synchronisatie ingeschakeld: modellen worden periodiek vernieuwd", "autoSyncDisabled": "Automatische synchronisatie uitgeschakeld", @@ -5439,17 +5454,17 @@ "interceptionLoadError": "Laden van onderscheppingsinstellingen mislukt: {error}", "interceptionSaveError": "Opslaan van onderscheppingsinstellingen mislukt: {error}", "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "Adverteer de modellen van deze provider onder claude/<provider>/<model> mirror-id's zodat het gateway-modelontdekkingsmechanisme van Claude Code ze kan weergeven. Standaard uitgeschakeld — het inschakelen hiervan verdubbelt de catalogusvermeldingen voor alle klanten.", - "ccAliasProviderLevelLabel": "Provider standaard", - "ccAliasModelOverridesLabel": "Per-model overschrijvingen", - "ccAliasModelOverrideAriaLabel": "Overschrijving voor {modelId}", - "ccAliasStateInherit": "Overnemen", - "ccAliasStateOn": "Aan", - "ccAliasStateOff": "Uit", - "ccAliasAddModelPlaceholder": "Model-id (bijv. gpt-4o)", - "ccAliasAddModelButton": "Voeg overschrijving toe", - "ccAliasLoadError": "Kon de discovery-alias instellingen niet laden: {error}", - "ccAliasSaveError": "Kon de discovery-alias instelling niet opslaan: {error}", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Verbind Galadriel met een API-sleutel.", "predibase": "$25 gratis proeftegoed (30 dagen geldig)", "chenzk": "OpenAI-compatibele gateway met een live modelcatalogus op chenzk.top.", - "freepik": "Genereer afbeeldingen met de Mystic API van Freepik.", + "magnific": "Genereer afbeeldingen met de Mystic API van Freepik.", "freetheai": "Gratis OpenAI-compatibele gateway met ondersteuning voor passthrough-modellen.", "g4f-gemini": "Gratis no-key g4f.space reverse proxy naar Gemini, beperkt tot 5 verzoeken per minuut.", "g4f-groq": "Gratis no-key g4f.space reverse proxy naar Groq, beperkt tot 5 verzoeken per minuut.", @@ -6209,6 +6224,7 @@ "claude": "Verbind Claude Code met de bestaande OAuth-flow.", "cline": "Verbind Cline met de bestaande OAuth-flow.", "cursor": "Verbind Cursor IDE met de bestaande OAuth-flow.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Verbind GitHub Copilot met de bestaande OAuth-flow.", "gitlab-duo": "OAuth-applicatie met ai_features + read_user scopes. Configureer GITLAB_DUO_OAUTH_CLIENT_ID en optioneel GITLAB_DUO_OAUTH_CLIENT_SECRET op deze OmniRoute-instantie.", "kilocode": "Verbind Kilo Code met de bestaande OAuth-flow.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "In afkoelperiode", "codexPoolUsed": "gebruikt", "codexPoolUntil": "Tot {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonieme fallback", "anonymousFallbackDesc": "Wanneer alle geconfigureerde verbindingen zijn uitgeput (quota, tegoeden of vervaldatum), gebruik tijdelijk de keyless-laag van deze provider. Zet uit om deze provider over te slaan in plaats van anonieme verzoeken te verzenden - aanbevolen wanneer de keyless-laag deze afwijst (401).", "anonymousFallbackEnabled": "Anonieme fallback ingeschakeld voor {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Instellingen voor opgeslagen model-eindpunt", "searchByModelAria": "Zoeken op model", "selectSupportedEndpoint": "Selecteer ten minste één ondersteunde eindpunt", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Auto-fetch van upstreammodel uitgeschakeld", - "autoFetchModelsTooltip": "Haal upstream-modellen op en cache ze indien nodig", - "autoFetchModels": "Automatisch upstream-modellen ophalen", - "autoFetchModelsEnabled": "Upstream model auto-fetch ingeschakeld", - "autoFetchModelsToggleFailed": "Kon upstream model auto-fetch niet omzetten", - "overridesUpstreamModelHint": "Jouw instellingen overschrijven dit upstream model", - "overridesUpstreamModel": "Overschrijft upstream", - "autoFetchModelsPartialFailure": "Sommige verbindingen zijn bijgewerkt, maar het automatisch ophalen van het upstream-model is niet overal gewijzigd", - "resetToUpstreamDefaults": "Herstel upstream standaardinstellingen", - "resetToUpstreamDefaultsSuccess": "Herstelde standaardinstellingen van upstream-model", - "resetToUpstreamDefaultsFailed": "Het is niet gelukt om de standaardinstellingen van het upstream-model te herstellen" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Instellingen", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Verboden trefwoorden", "customBannedSignalsDesc": "Aanvullende trefwoorden die detectie van permanente accountblokkering activeren. Ingebouwde trefwoorden zijn altijd van toepassing.", "customBannedSignalsPlaceholder": "bijv. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "geconfigureerd", "none": "Geen", "modelOverrideValuePlaceholder": "Numerieke waarde", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Sleutelwaarde toevoegen", "noModelOverrides": "Geen overschrijvingen geconfigureerd voor dit model.", "modelOverrideLoadFailed": "Laden van modeloverschrijvingen mislukt", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Beknopt CJK (文言)", "description": "Klassiek-Chinese ultra-beknopte stijl (alleen beschikbaar voor Chinees)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin- en willekeurige combinaties wisselen bij elk verzoek naar een andere verbinding in plaats van een heel gesprek vast te pinnen aan één verbinding op basis van de hash van het eerste bericht. Laat uitgeschakeld om prompt-cache-hits voor multi-turn chats te behouden. Overschrijvingen per combinatie hebben voorrang.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Maskeren van inloggegevens", "credentialRedactionDesc": "Maskeer API-sleutels, tokens en geheimen in context die naar providers wordt verzonden en in antwoorden.", "enableCredentialRedaction": "Maskeren van inloggegevens inschakelen", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Schakel de engine in", "enableDescription": "Draait als laatste in de stack (nadat RTK/Caveman de tekst opschoont, converteert OmniGlyph de rest naar afbeeldingen) en draait ook standalone via de omniglyph-modus. Dit is een preview en blijft standaard uitgeschakeld totdat de end-to-end-validatie is voltooid.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Opgeslagen.", "saveFailed": "Opslaan mislukt.", "enableAria": "Schakel de OmniGlyph-engine in", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "maand", "grokAdditionalCredits": "Aanvullende Credits", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budgetbeheer", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Eerste token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 77a2074b7e..502bbae12e 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visuell forespørselstidslinje", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "åpne", "close": "lukk" }, - "noResults": "Ingen resultater", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Ingen resultater" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kvoteandel", "discovery": "Oppdagelse", "freeProviderRankings": "Rangering av gratisleverandører", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Gratisnivåer", "gamification": "Spillifisering", "leaderboard": "Ledertavle", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Denne leverandøren er avviklet", "riskNotice": { "title": "Før du fortsetter", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Leverandør med forbehold om bruk — klikk for detaljer", "oauth": "Denne leverandøren bruker din offisielle produktøkt/OAuth, som ikke er autorisert for proxy-/rutingsbruk. Vi anbefaler ikke intensiv bruk av autonome agenter (OpenCloud-stil, lange flertrinnsflyter, store batcher) — oppstrømmen kan reagere med å begrense eller utestenge kontoen. Bruk på egen risiko.", "webCookie": "Denne leverandøren autentiserer via informasjonskapsler (cookies) fra nettøkten din. Oppstrømstjenesten kan ugyldiggjøre økten når som helst, noe som krever at du logger inn på nytt. Anbefales ikke for lange uovervåkede operasjoner. Bruk på egen risiko.", @@ -5107,9 +5111,9 @@ "cancel": "Avbryt" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Deaktivert", "enableProvider": "Aktiver leverandør", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Hopper over {count} eksisterende modeller", "autoSync": "Auto-synkronisering", "autoSyncShort": "Synkronisering", + "autoFetchModels": "Auto-hent oppstrøms modeller", + "autoFetchModelsTooltip": "Hent og cache upstream-modeller når det er nødvendig", + "autoFetchModelsEnabled": "Oppstrømsmodell automatisk henting aktivert", + "autoFetchModelsDisabled": "Oppstrømsmodell automatisk henting deaktivert", + "autoFetchModelsToggleFailed": "Kunne ikke aktivere automatisk henting av upstream-modell", + "autoFetchModelsPartialFailure": "Noen tilkoblinger ble oppdatert, men upstream-modellens auto-hent ble ikke endret overalt", + "overridesUpstreamModel": "Overstyrer upstream", + "overridesUpstreamModelHint": "Dine innstillinger overstyrer denne upstream-modellen", + "resetToUpstreamDefaults": "Gjenopprett upstream-standardinnstillinger", + "resetToUpstreamDefaultsSuccess": "Gjenopprettet upstream-modellinnstillinger", + "resetToUpstreamDefaultsFailed": "Kunne ikke gjenopprette standardinnstillinger for upstream-modellen", "autoSyncTooltip": "Oppdater modelllisten automatisk hver 24. time (kan konfigureres via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatisk synkronisering aktivert – modellene oppdateres med jevne mellomrom", "autoSyncDisabled": "Automatisk synkronisering er deaktivert", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Omskriv innebygde web_fetch-verktøykall til OmniRoutes /v1/web/fetch.", "interceptionLoadError": "Kunne ikke laste inn avskjæringsinnstillinger: {error}", "interceptionSaveError": "Kunne ikke lagre avskjæringsinnstillinger: {error}", - "ccAliasSectionTitle": "Eksponer i Claude Code (claude/…)", - "ccAliasSectionHint": "Reklamer denne leverandørens modeller under claude/<provider>/<model> speil-ID-er slik at Claude Codes gateway-modelloppdagelse kan liste dem. Av som standard — aktivering av dette dobler katalogoppføringene for alle klienter.", - "ccAliasProviderLevelLabel": "Leverandørstandard", - "ccAliasModelOverridesLabel": "Per-modell overstyringer", - "ccAliasModelOverrideAriaLabel": "Overstyring for {modelId}", - "ccAliasStateInherit": "Arv", - "ccAliasStateOn": "På", - "ccAliasStateOff": "Av", - "ccAliasAddModelPlaceholder": "Modell-ID (f.eks. gpt-4o)", - "ccAliasAddModelButton": "Legg til overstyring", - "ccAliasLoadError": "Kunne ikke laste inn discovery-alias-innstillinger: {error}", - "ccAliasSaveError": "Kunne ikke lagre discovery-alias-innstillingen: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Koble til Galadriel med en API-nøkkel.", "predibase": "$25 gratis prøveperiode-kreditt (30 dagers gyldighet)", "chenzk": "OpenAI-kompatibel gateway med en live modellkatalog på chenzk.top.", - "freepik": "Generer bilder med Freepiks Mystic-API.", + "magnific": "Generer bilder med Freepiks Mystic-API.", "freetheai": "Gratis OpenAI-kompatibel gateway med passthrough-modellstøtte.", "g4f-gemini": "Gratis nøkkelfri g4f.space reverse proxy til Gemini, begrenset til 5 forespørsler per minutt.", "g4f-groq": "Gratis nøkkelfri g4f.space reverse proxy til Groq, begrenset til 5 forespørsler per minutt.", @@ -6209,6 +6224,7 @@ "claude": "Koble til Claude Code med den eksisterende OAuth-flyten.", "cline": "Koble til Cline med den eksisterende OAuth-flyten.", "cursor": "Koble til Cursor IDE med den eksisterende OAuth-flyten.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Koble til GitHub Copilot med den eksisterende OAuth-flyten.", "gitlab-duo": "OAuth-applikasjon med ai_features + read_user-scopes. Konfigurer GITLAB_DUO_OAUTH_CLIENT_ID og eventuelt GITLAB_DUO_OAUTH_CLIENT_SECRET på denne OmniRoute-instansen.", "kilocode": "Koble til Kilo Code med den eksisterende OAuth-flyten.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "I nedkjølingsperiode", "codexPoolUsed": "brukt", "codexPoolUntil": "Til {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonym fallback", "anonymousFallbackDesc": "Når alle konfigurerte tilkoblinger er brukt opp (kvote, kreditter eller utløp), bruk midlertidig denne leverandørens nøkkelløse nivå. Slå av for å hoppe over denne leverandøren i stedet for å sende anonyme forespørsel — anbefales når det nøkkelløse nivået avviser dem (401).", "anonymousFallbackEnabled": "Anonym fallback aktivert for {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Innstillinger for lagrede modellendepunkter", "searchByModelAria": "Søk etter modell", "selectSupportedEndpoint": "Velg minst ett støttet endepunkt", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsTooltip": "Hent og cache upstream-modeller når det er nødvendig", - "autoFetchModelsEnabled": "Oppstrømsmodell automatisk henting aktivert", - "autoFetchModelsDisabled": "Oppstrømsmodell automatisk henting deaktivert", - "autoFetchModels": "Auto-hent oppstrøms modeller", - "autoFetchModelsToggleFailed": "Kunne ikke aktivere automatisk henting av upstream-modell", - "overridesUpstreamModel": "Overstyrer upstream", - "autoFetchModelsPartialFailure": "Noen tilkoblinger ble oppdatert, men upstream-modellens auto-hent ble ikke endret overalt", - "overridesUpstreamModelHint": "Dine innstillinger overstyrer denne upstream-modellen", - "resetToUpstreamDefaultsSuccess": "Gjenopprettet upstream-modellinnstillinger", - "resetToUpstreamDefaults": "Gjenopprett upstream-standardinnstillinger", - "resetToUpstreamDefaultsFailed": "Kunne ikke gjenopprette standardinnstillinger for upstream-modellen" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Innstillinger", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Forbudte nøkkelord", "customBannedSignalsDesc": "Ytterligere nøkkelord som utløser deteksjon av permanent kontoutestengelse. Innebygde nøkkelord gjelder alltid.", "customBannedSignalsPlaceholder": "f.eks. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "konfigurert", "none": "Ingen", "modelOverrideValuePlaceholder": "Numerisk verdi", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Legg til nøkkelverdi", "noModelOverrides": "Ingen overstyringer er konfigurert for denne modellen.", "modelOverrideLoadFailed": "Kunne ikke laste modelloverstyringer", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Kortfattet CJK (文言)", "description": "Klassisk-kinesisk ultrakortfattet stil (kun tilgjengelig for kinesisk)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin- og tilfeldige kombinasjoner roterer til en annen tilkobling for hver forespørsel i stedet for å låse en hel samtale til én tilkobling basert på hashen til den første meldingen. La være av for å bevare prompt-cache-treff for samtaler med flere runder. Overstyringer per kombinasjon har forrang.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Sladding av legitimasjon", "credentialRedactionDesc": "Sladd API-nøkler, tokener og hemmeligheter fra kontekst som sendes til leverandører, og fra svar.", "enableCredentialRedaction": "Aktiver sladding av legitimasjon", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Aktiver motoren", "enableDescription": "Kjører sist i stakken (etter at RTK/Caveman renser teksten, og OmniGlyph konverterer resten til bilder) og kjører også frittstående via omniglyph-modus. Dette er en forhåndsvisning og forblir deaktivert som standard til end-til-end-validering er fullført.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Lagret.", "saveFailed": "Kunne ikke lagre.", "enableAria": "Aktiver OmniGlyph-motoren", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maks", "grokAutoTopUpMonth": "måned", "grokAdditionalCredits": "Ytterligere Krediteringer", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Fullmakt", "budgetManagement": "Budsjettstyring", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Første token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index cfe0a6e006..5285b3d18c 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visual na kahilingan ng timeline", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "buksan", "close": "isara" }, - "noResults": "Walang resulta", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Walang resulta" }, "webhooks": { "title": "Mga Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "Quota Share", "discovery": "Pagtuklas", "freeProviderRankings": "Mga Ranggo ng Libreng Provider", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Mga Libreng Tier", "gamification": "Gamification", "leaderboard": "Leaderboard", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Ang provider na ito ay hindi na ginagamit", "riskNotice": { "title": "Bago magpatuloy", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provider na may mga paalala sa paggamit — i-click para sa mga detalye", "oauth": "Gumagamit ang provider na ito ng iyong opisyal na session ng produkto/OAuth, na hindi awtorisado para sa paggamit ng proxy/router. Hindi namin inirerekomenda ang masinsinang paggamit ng autonomous agent (estilong OpenCloud, mahabang multi-step na flow, malalaking batch) — maaaring tumugon ang upstream sa pamamagitan ng paghihigpit o pag-ban sa account. Gamitin sa sarili mong panganib.", "webCookie": "Nagpapatotoo ang provider na ito sa pamamagitan ng iyong mga cookie sa web session. Maaaring pawalang-bisa ng upstream na serbisyo ang session anumang oras, na nangangailangan sa iyong mag-log in muli. Hindi inirerekomenda para sa mahabang operasyon na walang bantay. Gamitin sa sarili mong panganib.", @@ -5107,9 +5111,9 @@ "cancel": "Kanselahin" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Hindi pinagana", "enableProvider": "Paganahin ang provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Pinapalampas ang {count} na umiiral na mga modelo", "autoSync": "Auto-Sync", "autoSyncShort": "I-sync", + "autoFetchModels": "Awtomatikong kunin ang mga upstream na modelo", + "autoFetchModelsTooltip": "Kunin at i-cache ang upstream models kapag kinakailangan", + "autoFetchModelsEnabled": "Naka-enable ang auto-fetch ng upstream model", + "autoFetchModelsDisabled": "Naka-disable ang auto-fetch ng upstream model", + "autoFetchModelsToggleFailed": "Nabigong i-toggle ang upstream model auto-fetch", + "autoFetchModelsPartialFailure": "Ilang koneksyon ang na-update, ngunit ang auto-fetch ng upstream model ay hindi nagbago sa lahat ng lugar", + "overridesUpstreamModel": "Pinalitan ang upstream", + "overridesUpstreamModelHint": "Ang iyong mga setting ay nangingibabaw sa modelong ito mula sa upstream", + "resetToUpstreamDefaults": "Ibalik ang mga default ng upstream", + "resetToUpstreamDefaultsSuccess": "Ibinalik ang mga default ng upstream model", + "resetToUpstreamDefaultsFailed": "Nabigong maibalik ang mga default ng upstream model", "autoSyncTooltip": "Awtomatikong i-refresh ang listahan ng modelo tuwing 24h (mako-configure sa pamamagitan ng MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Pinagana ang auto-sync — pana-panahong magre-refresh ang mga modelo", "autoSyncDisabled": "Na-disable ang auto-sync", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "I-rewrite ang mga native na tawag sa tool na web_fetch sa /v1/web/fetch ng OmniRoute.", "interceptionLoadError": "Hindi nai-load ang mga setting ng interception: {error}", "interceptionSaveError": "Hindi nai-save ang mga setting ng interception: {error}", - "ccAliasSectionTitle": "I-expose sa Claude Code (claude/…)", - "ccAliasSectionHint": "I-anunsyo ang mga modelo ng provider na ito sa ilalim ng claude/<provider>/<model> mirror ids upang ma-lista ang mga ito sa gateway model discovery ng Claude Code. Off sa default — ang pag-enable nito ay nagdodoble ng mga entry sa katalogo para sa lahat ng kliyente.", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", "ccAliasProviderLevelLabel": "Provider default", - "ccAliasModelOverridesLabel": "Mga Override Bawat Modelo", - "ccAliasModelOverrideAriaLabel": "Override para sa {modelId}", - "ccAliasStateInherit": "Mamana", - "ccAliasStateOn": "Sa", - "ccAliasStateOff": "Patay", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "Magdagdag ng override", - "ccAliasLoadError": "Nabigong i-load ang mga setting ng discovery-alias: {error}", - "ccAliasSaveError": "Nabigong i-save ang setting ng discovery-alias: {error}", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Ikonekta ang Galadriel gamit ang isang API key.", "predibase": "$25 na libreng trial credits (30-araw na validity)", "chenzk": "OpenAI-compatible na gateway na may live model catalog sa chenzk.top.", - "freepik": "Bumuo ng mga larawan gamit ang Mystic API ng Freepik.", + "magnific": "Bumuo ng mga larawan gamit ang Mystic API ng Freepik.", "freetheai": "Libreng OpenAI-compatible na gateway na may suporta sa passthrough model.", "g4f-gemini": "Libreng no-key na g4f.space reverse proxy sa Gemini, limitado sa 5 kahilingan bawat minuto.", "g4f-groq": "Libreng no-key na g4f.space reverse proxy sa Groq, limitado sa 5 kahilingan bawat minuto.", @@ -6209,6 +6224,7 @@ "claude": "Ikonekta ang Claude Code gamit ang umiiral na OAuth flow.", "cline": "Ikonekta ang Cline gamit ang umiiral na OAuth flow.", "cursor": "Ikonekta ang Cursor IDE gamit ang umiiral na OAuth flow.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Ikonekta ang GitHub Copilot gamit ang umiiral na OAuth flow.", "gitlab-duo": "OAuth application na may ai_features + read_user scopes. I-configure ang GITLAB_DUO_OAUTH_CLIENT_ID at opsyonal ang GITLAB_DUO_OAUTH_CLIENT_SECRET sa OmniRoute instance na ito.", "kilocode": "Ikonekta ang Kilo Code gamit ang umiiral na OAuth flow.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Nasa panahon ng paghihintay", "codexPoolUsed": "nagamit", "codexPoolUntil": "Hanggang {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonymous fallback", "anonymousFallbackDesc": "Kapag naubos na ang lahat ng nakatakdang koneksyon (quota, kredito, o pag-expire), pansamantalang gamitin ang keyless tier ng provider na ito. Patayin upang laktawan ang provider na ito sa halip na magpadala ng mga hindi nagpapakilalang kahilingan — inirerekomenda kapag tinanggihan ng keyless tier ang mga ito (401).", "anonymousFallbackEnabled": "Naka-enable ang anonymous fallback para sa {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Naka-save na mga setting ng endpoint ng modelo", "searchByModelAria": "Maghanap ayon sa modelo", "selectSupportedEndpoint": "Pumili ng hindi bababa sa isang sinusuportahang endpoint", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Naka-disable ang auto-fetch ng upstream model", - "autoFetchModelsTooltip": "Kunin at i-cache ang upstream models kapag kinakailangan", - "autoFetchModelsEnabled": "Naka-enable ang auto-fetch ng upstream model", - "autoFetchModels": "Awtomatikong kunin ang mga upstream na modelo", - "autoFetchModelsToggleFailed": "Nabigong i-toggle ang upstream model auto-fetch", - "overridesUpstreamModel": "Pinalitan ang upstream", - "overridesUpstreamModelHint": "Ang iyong mga setting ay nangingibabaw sa modelong ito mula sa upstream", - "resetToUpstreamDefaults": "Ibalik ang mga default ng upstream", - "resetToUpstreamDefaultsSuccess": "Ibinalik ang mga default ng upstream model", - "autoFetchModelsPartialFailure": "Ilang koneksyon ang na-update, ngunit ang auto-fetch ng upstream model ay hindi nagbago sa lahat ng lugar", - "resetToUpstreamDefaultsFailed": "Nabigong maibalik ang mga default ng upstream model" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Mga setting", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Mga Banned na Keyword", "customBannedSignalsDesc": "Mga karagdagang keyword na nagti-trigger ng pagtukoy sa permanenteng pag-ban ng account. Palaging nalalapat ang mga built-in na keyword.", "customBannedSignalsPlaceholder": "hal. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "naka-configure", "none": "Wala", "modelOverrideValuePlaceholder": "Numerikong halaga", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Magdagdag ng key value", "noModelOverrides": "Walang naka-configure na mga override para sa modelong ito.", "modelOverrideLoadFailed": "Bigo sa pag-load ng mga override ng modelo", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Maikling CJK (文言)", "description": "Klasikong Tsino na ultra-maikling estilo (magagamit lamang para sa Tsino)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Ang mga round-robin at random combo ay umiikot sa ibang koneksyon sa bawat request sa halip na i-pin ang buong pag-uusap sa isang koneksyon sa pamamagitan ng hash ng unang mensahe. Iwanang naka-off upang mapanatili ang mga prompt-cache hit para sa mga multi-turn chat. Mas nangingibabaw ang mga per-combo override.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Pag-redact ng Kredensyal", "credentialRedactionDesc": "I-redact ang mga API key, token, at secret mula sa kontekstong ipinadala sa mga provider at mula sa mga tugon.", "enableCredentialRedaction": "I-enable ang credential redaction", @@ -8621,6 +8623,27 @@ }, "enableTitle": "I-enable ang engine", "enableDescription": "Tumatakbo nang huli sa stack (pagkatapos linisin ng RTK/Caveman ang text, kino-convert ng OmniGlyph ang natitira sa mga imahe) at tumatakbo rin nang standalone sa pamamagitan ng omniglyph mode. Ito ay isang preview at nananatiling naka-off by default hanggang sa makumpleto ang end-to-end na pagpapatunay.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Nai-save.", "saveFailed": "Hindi mai-save.", "enableAria": "I-enable ang OmniGlyph engine", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "buwan", "grokAdditionalCredits": "Karagdagang Kredito", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Magtotroso", "proxyTab": "Proxy", "budgetManagement": "Pamamahala ng Badyet", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Unang Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 7fb9dbc363..e29e73670e 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Logi konsoli", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Wizualny harmonogram żądań", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Routing globalny", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "otwórz", "close": "zamknij" }, - "noResults": "Brak wyników", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Brak wyników" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Udział w limicie", "discovery": "Odkrywanie", "freeProviderRankings": "Rankingi darmowych dostawców", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Bezpłatne pakiety", "gamification": "Grywalizacja", "leaderboard": "Tabela liderów", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Wybierz, jak żądania są rozdzielane między models - dostępnych jest 14 strategii", "wizardStep4Title": "Przejrzyj i zapisz", "wizardStep4Desc": "Przejrzyj konfigurację i aktywuj combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "Wł.", "emailVisibilityStateOff": "Wył.", "reorderHandle": "Przeciągnij, aby zmienić kolejność", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Ten provider jest przestarzały", "riskNotice": { "title": "Przed kontynuowaniem", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provider z zastrzeżeniami dotyczącymi użytkowania — kliknij, aby uzyskać szczegóły", "oauth": "Ten provider korzysta z oficjalnej sesji produktu/OAuth, która nie jest autoryzowana do użytku jako proxy/router. Nie zalecamy intensywnego korzystania z autonomicznych agentów (w stylu OpenCloud, długich wieloetapowych przepływów, dużych partii) — upstream może zareagować ograniczeniem lub zablokowaniem konta. Użycie na własne ryzyko.", "webCookie": "Ten provider uwierzytelnia się za pomocą plików cookie sesji internetowej. Usługa upstream może unieważnić sesję w dowolnym momencie, co będzie wymagać ponownego zalogowania. Opcja ta nie jest zalecana do długich operacji bez nadzoru. Użycie na własne ryzyko.", @@ -5107,9 +5111,9 @@ "cancel": "Anuluj" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Wyłączone", "enableProvider": "Włącz provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Pomijanie {count} istniejących models", "autoSync": "Auto-Sync", "autoSyncShort": "Synchronizuj", + "autoFetchModels": "Automatyczne pobieranie modeli upstream", + "autoFetchModelsTooltip": "Pobierz i przechowuj modele upstream w razie potrzeby", + "autoFetchModelsEnabled": "Włączone automatyczne pobieranie modelu upstream", + "autoFetchModelsDisabled": "Automatyczne pobieranie modelu upstream wyłączone", + "autoFetchModelsToggleFailed": "Nie udało się przełączyć automatycznego pobierania modelu upstream", + "autoFetchModelsPartialFailure": "Niektóre połączenia zostały zaktualizowane, ale automatyczne pobieranie modelu upstream nie zostało zmienione wszędzie", + "overridesUpstreamModel": "Nadpisuje upstream", + "overridesUpstreamModelHint": "Twoje ustawienia nadpisują ten model upstream", + "resetToUpstreamDefaults": "Przywróć domyślne ustawienia upstream", + "resetToUpstreamDefaultsSuccess": "Przywrócono domyślne ustawienia modelu upstream", + "resetToUpstreamDefaultsFailed": "Nie udało się przywrócić domyślnych ustawień modelu upstream", "autoSyncTooltip": "Automatyczne odświeżanie listy models co 24h (konfigurowalne przez MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync włączony — models będą odświeżane okresowo", "autoSyncDisabled": "Auto-sync wyłączony", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Przepisywanie natywnych wywołań narzędzi web_fetch do /v1/web/fetch w OmniRoute.", "interceptionLoadError": "Nie udało się załadować ustawień przechwytywania: {error}", "interceptionSaveError": "Nie udało się zapisać ustawień przechwytywania: {error}", - "ccAliasSectionTitle": "Eksponuj w Claude Code (claude/…)", - "ccAliasSectionHint": "Reklamuj modele tego dostawcy pod identyfikatorami lustrzanymi claude/<provider>/<model>, aby model odkrywania bramy Claude Code mógł je wyświetlić. Domyślnie wyłączone — włączenie tego podwaja wpisy w katalogu dla wszystkich klientów.", - "ccAliasProviderLevelLabel": "Domyślny dostawca", - "ccAliasModelOverridesLabel": "Nadpisy dla poszczególnych modeli", - "ccAliasModelOverrideAriaLabel": "Nadpisz dla {modelId}", - "ccAliasStateInherit": "Dziedzicz", - "ccAliasStateOn": "Włączone", - "ccAliasStateOff": "Wyłączone", - "ccAliasAddModelPlaceholder": "Identyfikator modelu (np. gpt-4o)", - "ccAliasAddModelButton": "Dodaj nadpisanie", - "ccAliasLoadError": "Nie udało się załadować ustawień discovery-alias: {error}", - "ccAliasSaveError": "Nie udało się zapisać ustawienia discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Dodatkowe nagłówki upstream", "compatUpstreamHeadersHint": "Ustawienie o wysokich uprawnieniach — ten sam poziom zaufania, co edycja danych uwierzytelniających API dla provider; powinno być używane wyłącznie przez zaufanych administratorów. Scalane po dodaniu autoryzacji przez OmniRoute na podstawie klucza API dla provider. Jeśli niestandardowy nagłówek ma taką samą nazwę jak istniejący (np. Authorization), wprowadzona wartość w pełni zastępuje automatycznie wygenerowany nagłówek (w tym Bearer token) — upstream zobaczy tylko wpisaną wartość, a nie klucz z ustawień. Błędna konfiguracja może spowodować błąd 401 lub uszkodzenie autoryzacji upstream. Jeden wiersz na nagłówek (np. dodatkowe Authentication dla niektórych bramek). Najedź myszą lub kliknij pole, aby wyświetlić podgląd. Zapisuje się po utracie fokusu (blur), kliknięciu poza obszarem lub zamknięciu tego panelu.", "compatUpstreamHeaderName": "Nazwa nagłówka", @@ -6194,7 +6209,7 @@ "galadriel": "Połącz z Galadriel za pomocą klucza API.", "predibase": "$25 darmowych środków próbnych (ważność 30 dni)", "chenzk": "Brama zgodna z OpenAI z aktywnym katalogiem modeli na chenzk.top.", - "freepik": "Generuj obrazy za pomocą Mystic API od Freepik.", + "magnific": "Generuj obrazy za pomocą Mystic API od Freepik.", "freetheai": "Darmowa brama zgodna z OpenAI z obsługą modeli w trybie passthrough.", "g4f-gemini": "Darmowe, niewymagające klucza reverse proxy g4f.space do Gemini, z limitem do 5 żądań na minutę.", "g4f-groq": "Darmowe, niewymagające klucza reverse proxy g4f.space do Groq, z limitem do 5 żądań na minutę.", @@ -6209,6 +6224,7 @@ "claude": "Połącz Claude Code za pomocą istniejącego przepływu OAuth.", "cline": "Połącz Cline za pomocą istniejącego przepływu OAuth.", "cursor": "Połącz Cursor IDE za pomocą istniejącego przepływu OAuth.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Połącz GitHub Copilot za pomocą istniejącego przepływu OAuth.", "gitlab-duo": "Aplikacja OAuth z zakresami ai_features + read_user. Skonfiguruj GITLAB_DUO_OAUTH_CLIENT_ID i opcjonalnie GITLAB_DUO_OAUTH_CLIENT_SECRET w tej instancji OmniRoute.", "kilocode": "Połącz Kilo Code za pomocą istniejącego przepływu OAuth.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "W okresie oczekiwania", "codexPoolUsed": "wykorzystano", "codexPoolUntil": "Do {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonimowe zapasowe", "anonymousFallbackDesc": "Gdy wszystkie skonfigurowane połączenia są wyczerpane (kwota, kredyty lub wygaśnięcie), tymczasowo użyj bezkluczowego poziomu tego dostawcy. Wyłącz, aby pominąć tego dostawcę zamiast wysyłać anonimowe żądania — zalecane, gdy bezkluczowy poziom je odrzuca (401).", "anonymousFallbackEnabled": "Anonimowe przełączanie włączone dla {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Ustawienia punktu końcowego zapisanego modelu", "searchByModelAria": "Szukaj według modelu", "selectSupportedEndpoint": "Wybierz przynajmniej jeden obsługiwany punkt końcowy", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Automatyczne pobieranie modeli upstream", - "autoFetchModelsEnabled": "Włączone automatyczne pobieranie modelu upstream", - "autoFetchModelsTooltip": "Pobierz i przechowuj modele upstream w razie potrzeby", - "autoFetchModelsDisabled": "Automatyczne pobieranie modelu upstream wyłączone", - "overridesUpstreamModel": "Nadpisuje upstream", - "overridesUpstreamModelHint": "Twoje ustawienia nadpisują ten model upstream", - "autoFetchModelsPartialFailure": "Niektóre połączenia zostały zaktualizowane, ale automatyczne pobieranie modelu upstream nie zostało zmienione wszędzie", - "autoFetchModelsToggleFailed": "Nie udało się przełączyć automatycznego pobierania modelu upstream", - "resetToUpstreamDefaultsSuccess": "Przywrócono domyślne ustawienia modelu upstream", - "resetToUpstreamDefaults": "Przywróć domyślne ustawienia upstream", - "resetToUpstreamDefaultsFailed": "Nie udało się przywrócić domyślnych ustawień modelu upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Ustawienia", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Trwałe oznaczanie połączeń provider jako dezaktywowane, jeśli zwrócą one określone końcowe sygnały blokady (np. HTTP 403 'verify your account'). Spowoduje to usunięcie ich z rotacji combo.", "autoDisableThreshold": "Próg blokady", "autoDisableThresholdDesc": "Liczba kolejnych sygnałów blokady wymagana do trwałej dezaktywacji.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Zablokowane słowa kluczowe", "customBannedSignalsDesc": "Dodatkowe słowa kluczowe wyzwalające wykrywanie trwałej blokady konta. Wbudowane słowa kluczowe mają zawsze zastosowanie.", "customBannedSignalsPlaceholder": "np. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "skonfigurowano", "none": "Brak", "modelOverrideValuePlaceholder": "Wartość liczbowa", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Dodaj wartość klucza", "noModelOverrides": "Brak skonfigurowanych nadpisań dla tego model.", "modelOverrideLoadFailed": "Nie udało się załadować nadpisań model", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Zwięzłe CJK (文言)", "description": "Klasyczny chiński styl ultra-zwięzły (dostępny tylko dla języka chińskiego)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "combos typu round-robin i random zmieniają połączenie przy każdym żądaniu, zamiast przypinać całą konwersację do jednego połączenia na podstawie hasha pierwszej wiadomości. Pozostaw wyłączone, aby zachować trafienia prompt-cache dla wieloturowych czatów. Nadpisania per-combo mają pierwszeństwo.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Ukrywanie danych uwierzytelniających", "credentialRedactionDesc": "Ukrywaj klucze API, tokeny i sekrety w kontekście wysyłanym do dostawców oraz w odpowiedziach.", "enableCredentialRedaction": "Włącz ukrywanie danych uwierzytelniających", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Włącz silnik", "enableDescription": "Uruchamia się jako ostatni w stosie (po tym, jak RTK/Caveman oczyści tekst, a OmniGlyph skonwertuje resztę na obrazy), a także działa samodzielnie w trybie omniglyph. To jest wersja zapoznawcza i pozostaje domyślnie wyłączona do czasu zakończenia pełnej walidacji.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Zapisano.", "saveFailed": "Nie można zapisać.", "enableAria": "Włącz silnik OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksymalny", "grokAutoTopUpMonth": "miesiąc", "grokAdditionalCredits": "Dodatkowe Kredyty", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Zarządzanie budżetem", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Pierwszy Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index b3fe3125dd..4ba29762d0 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Linha do tempo de solicitação visual", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1282,8 +1284,6 @@ "resilienceConnectionsSubtitle": "Cooldown, disjuntor, estado de bloqueio", "settingsModalityBridge": "Ponte de Modalidade", "settingsModalityBridgeSubtitle": "Fallback de imagem/áudio → texto para modelos apenas de texto", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations", "commandPalette": { "title": "Paleta de Comandos", "searchPlaceholder": "Pesquisar páginas, configurações, ferramentas...", @@ -1800,6 +1800,11 @@ "updateStarted": "Atualização iniciada...", "reloadingPageAutomatically": "Recarregando a página automaticamente...", "providerTopology": "Topologia do provedor", + "recentRequests": "Requisições recentes", + "recentRequestsEmpty": "Nenhuma requisição ainda.", + "recentRequestsModel": "Modelo", + "recentRequestsTokens": "Entrada / Saída", + "recentRequestsWhen": "Quando", "downloadDmg": "Baixar DMG (macOS)", "downloadDmgDescription": "Uma nova versão do aplicativo de desktop OmniRoute está disponível. Por favor, baixe e instale o instalador DMG para macOS para atualizar (atual: v{version}).", "downloadExe": "Baixar EXE (Windows)", @@ -3606,6 +3611,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5106,7 @@ "deprecatedProvider": "Este provedor foi descontinuado", "riskNotice": { "title": "Antes de continuar", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provider com restrições de uso — clique para detalhes", "oauth": "Este provider usa sua sessão/OAuth oficial do produto, que não autoriza uso em proxy/router. Não recomendamos uso intensivo em agentes autônomos (estilo OpenCloud, multi-passos longos, batches grandes) — o upstream pode reagir restringindo ou banindo a conta. Use por sua conta e risco.", "webCookie": "Este provider autentica através dos cookies da sua sessão web. O serviço upstream pode invalidar a sessão a qualquer momento, exigindo re-login. Não recomendado para operações longas e não-supervisionadas. Use por sua conta e risco.", @@ -5107,9 +5116,9 @@ "cancel": "Cancelar" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Desativado", "enableProvider": "Ativar provedor", @@ -5233,6 +5242,17 @@ "skippingExistingModels": "Ignorando {count} modelos existentes", "autoSync": "Sincronização automática", "autoSyncShort": "Sincronizar", + "autoFetchModels": "Buscar automaticamente modelos upstream", + "autoFetchModelsTooltip": "Busque e armazene em cache os modelos upstream quando necessário", + "autoFetchModelsEnabled": "Modelo upstream de auto-busca habilitado", + "autoFetchModelsDisabled": "Busca automática do modelo upstream desativada", + "autoFetchModelsToggleFailed": "Falha ao alternar a busca automática do modelo upstream", + "autoFetchModelsPartialFailure": "Algumas conexões foram atualizadas, mas a busca automática do modelo upstream não foi alterada em todos os lugares", + "overridesUpstreamModel": "Substitui upstream", + "overridesUpstreamModelHint": "Suas configurações substituem este modelo upstream", + "resetToUpstreamDefaults": "Restaurar padrões do upstream", + "resetToUpstreamDefaultsSuccess": "Restaurados os padrões do modelo upstream", + "resetToUpstreamDefaultsFailed": "Falha ao restaurar as configurações padrão do modelo upstream", "autoSyncTooltip": "Atualize automaticamente a lista de modelos a cada 24h (configurável via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Sincronização automática habilitada – os modelos serão atualizados periodicamente", "autoSyncDisabled": "Sincronização automática desativada", @@ -5438,18 +5458,18 @@ "interceptFetchHint": "Reescreve chamadas nativas de web_fetch para /v1/web/fetch da OmniRoute.", "interceptionLoadError": "Falha ao carregar configuração de interceptação: {error}", "interceptionSaveError": "Falha ao salvar configuração de interceptação: {error}", - "ccAliasSectionTitle": "Expose em Claude Code (claude/…)", - "ccAliasSectionHint": "Anuncie os modelos deste provedor sob claude/<provider>/<model> IDs de espelho para que a descoberta de modelos do gateway do Claude Code possa listá-los. Desativado por padrão — habilitar isso dobra as entradas do catálogo para todos os clientes.", - "ccAliasProviderLevelLabel": "Provedor padrão", - "ccAliasModelOverridesLabel": "Substituições por modelo", - "ccAliasModelOverrideAriaLabel": "Substituição para {modelId}", - "ccAliasStateInherit": "Herdar", - "ccAliasStateOn": "Ligado", - "ccAliasStateOff": "Desligado", - "ccAliasAddModelPlaceholder": "ID do modelo (por exemplo, gpt-4o)", - "ccAliasAddModelButton": "Adicionar substituição", - "ccAliasLoadError": "Falha ao carregar as configurações de discovery-alias: {error}", - "ccAliasSaveError": "Falha ao salvar a configuração de discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6214,7 @@ "galadriel": "Conecte o Galadriel com uma chave de API.", "predibase": "$25 em créditos de teste gratuitos (validade de 30 dias)", "chenzk": "Gateway compatível com OpenAI com um catálogo de modelos ao vivo em chenzk.top.", - "freepik": "Gere imagens com a API Mystic do Freepik.", + "magnific": "Gere imagens com a API Mystic do Freepik.", "freetheai": "Gateway gratuito compatível com OpenAI com suporte a modelos via passthrough.", "g4f-gemini": "Proxy reverso gratuito e sem chave do g4f.space para o Gemini, limitado a 5 solicitações por minuto.", "g4f-groq": "Proxy reverso gratuito e sem chave do g4f.space para o Groq, limitado a 5 solicitações por minuto.", @@ -6209,6 +6229,7 @@ "claude": "Conecte o Claude Code com o fluxo OAuth existente.", "cline": "Conecte o Cline com o fluxo OAuth existente.", "cursor": "Conecte o Cursor IDE com o fluxo OAuth existente.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Conecte o GitHub Copilot com o fluxo OAuth existente.", "gitlab-duo": "Aplicação OAuth com os escopos ai_features + read_user. Configure GITLAB_DUO_OAUTH_CLIENT_ID e, opcionalmente, GITLAB_DUO_OAUTH_CLIENT_SECRET nesta instância do OmniRoute.", "kilocode": "Conecte o Kilo Code com o fluxo OAuth existente.", @@ -6270,6 +6291,7 @@ "kimiOfficialSupporterTooltip": "A Kimi (Moonshot AI) é parceira oficial de lançamento do OmniRoute", "cheaperInferenceSupporterBadge": "Amigo do Código Aberto", "cheaperInferenceSupporterTooltip": "A Cheaper Inference apoia o OmniRoute como amiga do código aberto", + "kimiPartnerLinkNote": "Link de parceria — apoia o OmniRoute sem custo extra para você", "codexQuotaPools": "Pools de cotas do Codex", "codexPoolAvailable": "Disponível", "codexPoolPartiallyLimited": "Parcialmente limitado", @@ -6279,19 +6301,6 @@ "codexPoolCoolingDown": "Em período de espera", "codexPoolUsed": "usado", "codexPoolUntil": "Até {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", - "kimiPartnerLinkNote": "Link de parceria — apoia o OmniRoute sem custo extra para você", "anonymousFallbackTitle": "Fallback anônimo", "anonymousFallbackDesc": "Quando todas as conexões configuradas estiverem esgotadas (cota, créditos ou expiração), use temporariamente a camada sem chave deste provedor. Desative para ignorar este provedor em vez de enviar solicitações anônimas — recomendado quando a camada sem chave as rejeita (401).", "anonymousFallbackEnabled": "Fallback anônimo ativado para {provider}", @@ -6367,18 +6376,7 @@ "savedModelEndpointSettings": "Configurações do endpoint do modelo salvo", "searchByModelAria": "Pesquisar por modelo", "selectSupportedEndpoint": "Selecione pelo menos um endpoint suportado", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsEnabled": "Modelo upstream de auto-busca habilitado", - "autoFetchModelsDisabled": "Busca automática do modelo upstream desativada", - "autoFetchModels": "Buscar automaticamente modelos upstream", - "autoFetchModelsTooltip": "Busque e armazene em cache os modelos upstream quando necessário", - "autoFetchModelsToggleFailed": "Falha ao alternar a busca automática do modelo upstream", - "overridesUpstreamModel": "Substitui upstream", - "autoFetchModelsPartialFailure": "Algumas conexões foram atualizadas, mas a busca automática do modelo upstream não foi alterada em todos os lugares", - "overridesUpstreamModelHint": "Suas configurações substituem este modelo upstream", - "resetToUpstreamDefaults": "Restaurar padrões do upstream", - "resetToUpstreamDefaultsFailed": "Falha ao restaurar as configurações padrão do modelo upstream", - "resetToUpstreamDefaultsSuccess": "Restaurados os padrões do modelo upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Configurações", @@ -6597,12 +6595,12 @@ "autoDisableDescription": "Marca permanentemente conexões de provedor como desativadas quando retornam sinais terminais de banimento (ex.: HTTP 403 'verify your account'). Isso remove a conexão da rotação de combos.", "autoDisableThreshold": "Limite de banimento", "autoDisableThresholdDesc": "Quantidade de sinais consecutivos de banimento antes da desativação permanente.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Palavras-Chave Proibidas", "customBannedSignalsDesc": "Palavras-chave adicionais que acionam a detecção de banimento permanente da conta. Palavras-chave integradas sempre se aplicam.", "customBannedSignalsPlaceholder": "chave da API revogada", @@ -7210,6 +7208,7 @@ "configured": "configurado", "none": "Nenhum", "modelOverrideValuePlaceholder": "Valor numérico", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Adicionar valor da chave", "noModelOverrides": "Nenhuma substituição configurada para este modelo.", "modelOverrideLoadFailed": "Falha ao carregar substituições de modelo", @@ -7781,6 +7780,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK conciso (文言)", "description": "Estilo ultra-conciso em chinês clássico (disponível apenas para chinês)." @@ -8061,6 +8064,10 @@ "disableSessionStickinessDesc": "Combos round-robin e aleatórios alternam para uma conexão diferente a cada requisição, em vez de fixar toda a conversa em uma conexão pelo hash da primeira mensagem. Deixe desativado para preservar acertos de cache de prompt em conversas com múltiplos turnos. Sobrescritas por combo têm prioridade.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Redação de Credenciais", "credentialRedactionDesc": "Redija chaves de API, tokens e segredos do contexto enviado para provedores e das respostas.", "enableCredentialRedaction": "Ativar a ocultação de credenciais", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 9935bfe7ef..05441018a3 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Registos da Consola", "logsTimeline": "Linha do Tempo", "logsTimelineSubtitle": "Linha do tempo de pedidos visuais", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Encaminhamento Global", "mitmProxy": "Proxy MITM", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "abrir", "close": "fechar" }, - "noResults": "Sem resultados", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Sem resultados" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Partilha de Quota", "discovery": "Descoberta", "freeProviderRankings": "Rankings de Fornecedores Gratuitos", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Escalões Gratuitos", "gamification": "Gamificação", "leaderboard": "Tabela de classificação", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Rever e Guardar", "wizardStep4Desc": "Rever a configuração e ativar o combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "Ligado", "emailVisibilityStateOff": "Desligado", "reorderHandle": "Arrasta para reordenar", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Este provedor foi descontinuado", "riskNotice": { "title": "Antes de continuar", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Provedor com advertências de utilização — clique para detalhes", "oauth": "Este provedor utiliza a sua sessão oficial do produto/OAuth, que não está autorizada para utilização de proxy/router. Não recomendamos a utilização intensiva de agentes autónomos (estilo OpenCloud, fluxos longos de vários passos, grandes lotes) — o upstream pode reagir restringindo ou banindo a conta. Utilize por sua conta e risco.", "webCookie": "Este provedor autentica-se através dos cookies da sua sessão web. O serviço upstream pode invalidar a sessão a qualquer momento, exigindo que inicie sessão novamente. Não recomendado para operações longas sem supervisão. Utilize por sua conta e risco.", @@ -5107,9 +5111,9 @@ "cancel": "Cancelar" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Desativado", "enableProvider": "Habilitar provedor", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "A ignorar {count} modelos existentes", "autoSync": "Sincronização automática", "autoSyncShort": "Sincronizar", + "autoFetchModels": "Busca automática de modelos upstream", + "autoFetchModelsTooltip": "Buscar e armazenar em cache modelos upstream quando necessário", + "autoFetchModelsEnabled": "Modelo upstream de auto-busca ativado", + "autoFetchModelsDisabled": "Auto-busca do modelo upstream desativada", + "autoFetchModelsToggleFailed": "Falha ao alternar a busca automática do modelo upstream", + "autoFetchModelsPartialFailure": "Algumas ligações foram atualizadas, mas a busca automática do modelo upstream não foi alterada em todos os lugares", + "overridesUpstreamModel": "Substitui upstream", + "overridesUpstreamModelHint": "As suas definições substituem este modelo upstream", + "resetToUpstreamDefaults": "Restaurar as definições padrão do upstream", + "resetToUpstreamDefaultsSuccess": "Restaurados os valores padrão do modelo upstream", + "resetToUpstreamDefaultsFailed": "Falha ao restaurar as definições padrão do modelo upstream", "autoSyncTooltip": "Atualiza automaticamente a lista de modelos a cada 24 horas (configurável via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Sincronização automática ativada — modelos serão atualizados periodicamente", "autoSyncDisabled": "Sincronização automática desativada", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Reescrever chamadas de ferramentas nativas web_fetch para o /v1/web/fetch do OmniRoute.", "interceptionLoadError": "Falha ao carregar as definições de interceção: {error}", "interceptionSaveError": "Falha ao guardar as definições de interceção: {error}", - "ccAliasSectionTitle": "Expose em Claude Code (claude/…)", - "ccAliasSectionHint": "Anuncie os modelos deste fornecedor sob claude/<provider>/<model> IDs de espelho para que a descoberta de modelos do gateway do Claude Code os possa listar. Desativado por padrão — ativar isto duplica as entradas do catálogo para todos os clientes.", - "ccAliasProviderLevelLabel": "Fornecedor padrão", - "ccAliasModelOverridesLabel": "Substituições por modelo", - "ccAliasModelOverrideAriaLabel": "Substituição para {modelId}", - "ccAliasStateInherit": "Herdar", - "ccAliasStateOn": "Ligado", - "ccAliasStateOff": "Desligado", - "ccAliasAddModelPlaceholder": "Id do modelo (ex: gpt-4o)", - "ccAliasAddModelButton": "Adicionar substituição", - "ccAliasLoadError": "Falha ao carregar as definições de discovery-alias: {error}", - "ccAliasSaveError": "Falha ao salvar a configuração discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Cabeçalhos upstream extra", "compatUpstreamHeadersHint": "Definição de alto privilégio — mesmo nível de confiança que editar credenciais de API do fornecedor; só admins de confiança devem usar.", "compatUpstreamHeaderName": "Nome do cabeçalho", @@ -6194,7 +6209,7 @@ "galadriel": "Ligue a Galadriel com uma chave de API.", "predibase": "$25 em créditos de avaliação gratuita (validade de 30 dias)", "chenzk": "Gateway compatível com a OpenAI com um catálogo de modelos em tempo real em chenzk.top.", - "freepik": "Gere imagens com a API Mystic da Freepik.", + "magnific": "Gere imagens com a API Mystic da Freepik.", "freetheai": "Gateway gratuito compatível com a OpenAI com suporte a modelos passthrough.", "g4f-gemini": "Proxy inverso g4f.space gratuito e sem chave para o Gemini, limitado a 5 pedidos por minuto.", "g4f-groq": "Proxy inverso g4f.space gratuito e sem chave para o Groq, limitado a 5 pedidos por minuto.", @@ -6209,6 +6224,7 @@ "claude": "Ligar o Claude Code com o fluxo OAuth existente.", "cline": "Ligar o Cline com o fluxo OAuth existente.", "cursor": "Ligar o Cursor IDE com o fluxo OAuth existente.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Ligar o GitHub Copilot com o fluxo OAuth existente.", "gitlab-duo": "Aplicação OAuth com os âmbitos ai_features + read_user. Configure GITLAB_DUO_OAUTH_CLIENT_ID e, opcionalmente, GITLAB_DUO_OAUTH_CLIENT_SECRET nesta instância do OmniRoute.", "kilocode": "Ligar o Kilo Code com o fluxo OAuth existente.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Em período de espera", "codexPoolUsed": "utilizado", "codexPoolUntil": "Até {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback anónimo", "anonymousFallbackDesc": "Quando todas as conexões configuradas estiverem esgotadas (quota, créditos ou expiração), use temporariamente o nível sem chave deste fornecedor. Desative para ignorar este fornecedor em vez de enviar pedidos anónimos — recomendado quando o nível sem chave os rejeita (401).", "anonymousFallbackEnabled": "Fallback anónimo ativado para {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Definições do ponto de extremidade do modelo guardado", "searchByModelAria": "Pesquisar por modelo", "selectSupportedEndpoint": "Selecione pelo menos um endpoint suportado", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Auto-busca do modelo upstream desativada", - "autoFetchModelsTooltip": "Buscar e armazenar em cache modelos upstream quando necessário", - "autoFetchModelsEnabled": "Modelo upstream de auto-busca ativado", - "autoFetchModels": "Busca automática de modelos upstream", - "overridesUpstreamModel": "Substitui upstream", - "overridesUpstreamModelHint": "As suas definições substituem este modelo upstream", - "autoFetchModelsToggleFailed": "Falha ao alternar a busca automática do modelo upstream", - "autoFetchModelsPartialFailure": "Algumas ligações foram atualizadas, mas a busca automática do modelo upstream não foi alterada em todos os lugares", - "resetToUpstreamDefaults": "Restaurar as definições padrão do upstream", - "resetToUpstreamDefaultsSuccess": "Restaurados os valores padrão do modelo upstream", - "resetToUpstreamDefaultsFailed": "Falha ao restaurar as definições padrão do modelo upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Configurações", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Marca permanentemente conexões de provedor como desativadas quando retornam sinais terminais de banimento (ex.: HTTP 403 'verify your account'). Isso remove a conexão da rotação de combos.", "autoDisableThreshold": "Limite de banimento", "autoDisableThresholdDesc": "Quantidade de sinais consecutivos de banimento antes da desativação permanente.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Palavras-chave Proibidas", "customBannedSignalsDesc": "Palavras-chave adicionais que acionam a deteção de banimento permanente da conta. As palavras-chave integradas aplicam-se sempre.", "customBannedSignalsPlaceholder": "ex. chave de API revogada", @@ -7210,6 +7203,7 @@ "configured": "configurado", "none": "Nenhum", "modelOverrideValuePlaceholder": "Valor numérico", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Adicionar chave-valor", "noModelOverrides": "Nenhuma substituição configurada para este modelo.", "modelOverrideLoadFailed": "Falha ao carregar as substituições de modelo", @@ -7781,6 +7775,10 @@ "label": "Ponytail (dev sénior preguiçoso)", "description": "Disciplina de dev sénior preguiçoso: sobe a escada YAGNI, corrige a causa raiz, menor diff funcional." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK conciso (文言)", "description": "Estilo ultraconciso em chinês clássico (disponível apenas para chinês)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "As combinações round-robin e aleatórias alternam para uma ligação diferente a cada pedido em vez de fixarem uma conversa inteira a uma ligação através do hash da primeira mensagem. Deixe desativado para preservar os hits da cache de prompts em conversas com múltiplos turnos. As sobreposições por combinação têm precedência.", "promptCacheAffinity": "Encaminhamento por localidade de prompt-cache", "promptCacheAffinityDesc": "Prefere a mesma conta de fornecedor para chaves de prompt-cache correspondentes, preservando o failover de saúde e quota.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Ocultação de Credenciais", "credentialRedactionDesc": "Oculte chaves de API, tokens e segredos do contexto enviado para os fornecedores e das respostas.", "enableCredentialRedaction": "Ativar ocultação de credenciais", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Ativar o motor", "enableDescription": "Executa em último lugar na pilha (após o RTK/Caveman limpar o texto, o OmniGlyph converte o restante em imagens) e também executa de forma autónoma através do modo omniglyph. Esta é uma pré-visualização e permanece desativada por predefinição até que a validação de ponta a ponta esteja concluída.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Guardado.", "saveFailed": "Não foi possível guardar.", "enableAria": "Ativar o motor OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "máx", "grokAutoTopUpMonth": "mês", "grokAdditionalCredits": "Créditos Adicionais", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Registrador", "proxyTab": "Procurador", "budgetManagement": "Gestão Orçamentária", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Primeiro Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Falha ao guardar as definições locais do Radar", "guidedCombos": "Guided combos", "offers": "Ofertas", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13346,7 +13379,7 @@ "pollErrorStopped": "Sondagem interrompida: o servidor rejeitou o pedido (404/403).", "pollErrorTransient": "Erro ao obter dados: a repetir automaticamente.", "degraded": { - "message": "__MISSING__:Partial data: unavailable sources: {sources}", + "message": "Partial data: unavailable sources: {sources}", "source": { "database": "Base de Dados", "circuitBreaker": "Disjuntor", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# dia} other {# dias}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index cec84fa369..b8ef63cbb8 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Cronologia cererilor vizuale", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "deschide", "close": "închide" }, - "noResults": "Niciun rezultat", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Niciun rezultat" }, "webhooks": { "title": "Webhook-uri", @@ -1739,8 +1739,8 @@ "quotaShare": "Partajare cotă", "discovery": "Descoperire", "freeProviderRankings": "Clasament furnizori gratuiți", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Niveluri gratuite", "gamification": "Gamificare", "leaderboard": "Clasament", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Acest furnizor a fost retras", "riskNotice": { "title": "Înainte de a continua", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Furnizor cu avertismente de utilizare — faceți clic pentru detalii", "oauth": "Acest furnizor utilizează sesiunea oficială a produsului/OAuth, care nu este autorizată pentru utilizarea ca proxy/router. Nu recomandăm utilizarea intensivă a agenților autonomi (stil OpenCloud, fluxuri lungi cu mai mulți pași, loturi mari) — upstream-ul poate reacționa prin restricționarea sau blocarea contului. Utilizați pe propriul risc.", "webCookie": "Acest furnizor se autentifică prin cookie-urile sesiunii web. Serviciul upstream poate invalida sesiunea în orice moment, solicitându-vă să vă autentificați din nou. Nu este recomandat pentru operațiuni lungi nesupravegheate. Utilizați pe propriul risc.", @@ -5107,9 +5111,9 @@ "cancel": "Anulează" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Dezactivat", "enableProvider": "Activați furnizorul", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Se omit {count} modele existente", "autoSync": "Sincronizare automată", "autoSyncShort": "Sincronizează", + "autoFetchModels": "Obține automat modelele upstream", + "autoFetchModelsTooltip": "Recuperează și stochează modelele upstream atunci când este necesar", + "autoFetchModelsEnabled": "Modelul upstream auto-fetch activat", + "autoFetchModelsDisabled": "Modelul upstream auto-fetch dezactivat", + "autoFetchModelsToggleFailed": "Nu s-a reușit comutarea automată a preluării modelului upstream", + "autoFetchModelsPartialFailure": "Unele conexiuni au fost actualizate, dar modelul upstream auto-fetch nu a fost schimbat peste tot", + "overridesUpstreamModel": "Suprascrie upstream", + "overridesUpstreamModelHint": "Setările tale suprascriu acest model de bază", + "resetToUpstreamDefaults": "Restabilește valorile implicite upstream", + "resetToUpstreamDefaultsSuccess": "Restabilite valorile implicite ale modelului upstream", + "resetToUpstreamDefaultsFailed": "Restaurarea valorilor implicite ale modelului upstream a eșuat", "autoSyncTooltip": "Actualizează automat lista de modele la fiecare 24 de ore (configurabil prin MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Sincronizare automată activată — modelele se vor reîmprospăta periodic", "autoSyncDisabled": "Sincronizarea automată a fost dezactivată", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Rescrie apelurile native de instrument web_fetch către /v1/web/fetch al OmniRoute.", "interceptionLoadError": "Nu s-au putut încărca setările de interceptare: {error}", "interceptionSaveError": "Nu s-au putut salva setările de interceptare: {error}", - "ccAliasSectionTitle": "Expune în Claude Code (claude/…)", - "ccAliasSectionHint": "Publica modelele acestui furnizor sub claude/<provider>/<model> ID-uri mirror, astfel încât descoperirea modelului gateway al Claude Code să le poată lista. Dezactivat în mod implicit — activarea acestuia dublează intrările din catalog pentru toți clienții.", - "ccAliasProviderLevelLabel": "Provider implicit", - "ccAliasModelOverridesLabel": "Suprapuneri pe model per model", - "ccAliasModelOverrideAriaLabel": "Suprascriere pentru {modelId}", - "ccAliasStateInherit": "Moștenește", - "ccAliasStateOn": "Activat", - "ccAliasStateOff": "Oprit", - "ccAliasAddModelPlaceholder": "ID model (de exemplu, gpt-4o)", - "ccAliasAddModelButton": "Adaugă suprascriere", - "ccAliasLoadError": "Nu s-au putut încărca setările discovery-alias: {error}", - "ccAliasSaveError": "Nu s-a reușit salvarea setării discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Conectați Galadriel cu o cheie API.", "predibase": "Credite de încercare gratuită de 25 $ (valabilitate 30 de zile)", "chenzk": "Gateway compatibil cu OpenAI cu un catalog live de modele la chenzk.top.", - "freepik": "Generați imagini cu API-ul Mystic de la Freepik.", + "magnific": "Generați imagini cu API-ul Mystic de la Freepik.", "freetheai": "Gateway gratuit compatibil cu OpenAI cu suport pentru modele passthrough.", "g4f-gemini": "Reverse proxy gratuit fără cheie g4f.space către Gemini, limitat la 5 cereri pe minut.", "g4f-groq": "Reverse proxy gratuit fără cheie g4f.space către Groq, limitat la 5 cereri pe minut.", @@ -6209,6 +6224,7 @@ "claude": "Conectați Claude Code cu fluxul OAuth existent.", "cline": "Conectați Cline cu fluxul OAuth existent.", "cursor": "Conectați Cursor IDE cu fluxul OAuth existent.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Conectați GitHub Copilot cu fluxul OAuth existent.", "gitlab-duo": "Aplicație OAuth cu permisiunile ai_features + read_user. Configurați GITLAB_DUO_OAUTH_CLIENT_ID și opțional GITLAB_DUO_OAUTH_CLIENT_SECRET pe această instanță OmniRoute.", "kilocode": "Conectați Kilo Code cu fluxul OAuth existent.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "În perioada de așteptare", "codexPoolUsed": "utilizat", "codexPoolUntil": "Până la {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Fallback anonim", "anonymousFallbackDesc": "Când toate conexiunile configurate sunt epuizate (cota, credite sau expirare), folosiți temporar nivelul fără cheie al acestui furnizor. Dezactivați pentru a sări peste acest furnizor în loc de a trimite cereri anonime — recomandat atunci când nivelul fără cheie le respinge (401).", "anonymousFallbackEnabled": "Fallback anonim activat pentru {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Setările punctului final al modelului salvat", "searchByModelAria": "Caută după model", "selectSupportedEndpoint": "Selectați cel puțin un punct final acceptat", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Obține automat modelele upstream", - "autoFetchModelsDisabled": "Modelul upstream auto-fetch dezactivat", - "autoFetchModelsTooltip": "Recuperează și stochează modelele upstream atunci când este necesar", - "autoFetchModelsEnabled": "Modelul upstream auto-fetch activat", - "overridesUpstreamModel": "Suprascrie upstream", - "autoFetchModelsToggleFailed": "Nu s-a reușit comutarea automată a preluării modelului upstream", - "overridesUpstreamModelHint": "Setările tale suprascriu acest model de bază", - "autoFetchModelsPartialFailure": "Unele conexiuni au fost actualizate, dar modelul upstream auto-fetch nu a fost schimbat peste tot", - "resetToUpstreamDefaults": "Restabilește valorile implicite upstream", - "resetToUpstreamDefaultsSuccess": "Restabilite valorile implicite ale modelului upstream", - "resetToUpstreamDefaultsFailed": "Restaurarea valorilor implicite ale modelului upstream a eșuat" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Setări", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Cuvinte cheie interzise", "customBannedSignalsDesc": "Cuvinte cheie suplimentare care declanșează detectarea blocării permanente a contului. Cuvintele cheie integrate se aplică întotdeauna.", "customBannedSignalsPlaceholder": "de ex. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "configurat", "none": "Niciunul", "modelOverrideValuePlaceholder": "Valoare numerică", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Adaugă cheie-valoare", "noModelOverrides": "Nu sunt configurate suprascrieri pentru acest model.", "modelOverrideLoadFailed": "Eroare la încărcarea suprascrierilor de model", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK concis (文言)", "description": "Stil ultra-concis în chineza clasică (disponibil doar pentru chineză)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Combinațiile round-robin și aleatorii comută la o conexiune diferită la fiecare solicitare, în loc să fixeze o întreagă conversație la o singură conexiune pe baza hash-ului primului mesaj. Lăsați dezactivat pentru a păstra accesările din cache-ul de prompturi pentru conversațiile cu mai multe replici. Suprascrierile per combinație au prioritate.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Mascare date de autentificare", "credentialRedactionDesc": "Mascați cheile API, tokenurile și secretele din contextul trimis către furnizori și din răspunsuri.", "enableCredentialRedaction": "Activează redactarea credențialelor", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Activează motorul", "enableDescription": "Rulează ultimul în stivă (după ce RTK/Caveman curăță textul, OmniGlyph convertește restul în imagini) și rulează, de asemenea, de sine stătător prin modul omniglyph. Aceasta este o previzualizare și rămâne dezactivată în mod implicit până când validarea end-to-end este finalizată.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Salvat.", "saveFailed": "Nu s-a putut salva.", "enableAria": "Activează motorul OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "luna", "grokAdditionalCredits": "Credite Suplimentare", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Managementul bugetului", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Primul token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index ba9a4e812e..cf9ff5e8c2 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Визуальная временная шкала запросов", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "открыть", "close": "закрыть" }, - "noResults": "Нет результатов", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Нет результатов" }, "webhooks": { "title": "Вебхуки", @@ -1739,8 +1739,8 @@ "quotaShare": "Доля квоты", "discovery": "Обнаружение", "freeProviderRankings": "Рейтинг бесплатных провайдеров", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Бесплатные тарифы", "gamification": "Геймификация", "leaderboard": "Таблица лидеров", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Этот провайдер устарел", "riskNotice": { "title": "Перед продолжением", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Провайдер с ограничениями по использованию — нажмите для получения деталей", "oauth": "Этот провайдер использует вашу официальную сессию продукта/OAuth, которая не разрешена для использования с прокси/маршрутизатором. Мы не рекомендуем интенсивное использование автономных агентов (в стиле OpenCloud, длинные многошаговые потоки, большие партии) — вышестоящий сервис может отреагировать, ограничив или заблокировав аккаунт. Используйте на свой страх и риск.", "webCookie": "Этот провайдер аутентифицируется через ваши веб-сессионные куки. Внешний сервис может аннулировать сессию в любое время, требуя повторного входа в систему. Не рекомендуется для длительных unattended операций. Используйте на свой страх и риск.", @@ -5107,9 +5111,9 @@ "cancel": "Отмена" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Отключено", "enableProvider": "Включить провайдера", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Пропуск {count} существующих моделей", "autoSync": "Автосинхронизация", "autoSyncShort": "Синхронизация", + "autoFetchModels": "Автоматически получать модели из upstream", + "autoFetchModelsTooltip": "Получить и кэшировать модели upstream по мере необходимости", + "autoFetchModelsEnabled": "Включен автоматический выбор модели upstream", + "autoFetchModelsDisabled": "Автоматическое получение модели из upstream отключено", + "autoFetchModelsToggleFailed": "Не удалось переключить автоматическую выборку модели upstream", + "autoFetchModelsPartialFailure": "Некоторые соединения обновлены, но авто-загрузка модели upstream не была изменена везде", + "overridesUpstreamModel": "Переопределяет upstream", + "overridesUpstreamModelHint": "Ваши настройки переопределяют эту модель upstream", + "resetToUpstreamDefaults": "Восстановить настройки по умолчанию upstream", + "resetToUpstreamDefaultsSuccess": "Восстановлены настройки модели по умолчанию для upstream", + "resetToUpstreamDefaultsFailed": "Не удалось восстановить значения по умолчанию для модели upstream", "autoSyncTooltip": "Автоматически обновляет список моделей каждые 24 часа (настраивается через MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Автосинхронизация включена — модели будут периодически обновляться", "autoSyncDisabled": "Автосинхронизация отключена", @@ -5438,16 +5453,16 @@ "interceptFetchHint": "Перенаправлять нативные вызовы инструмента web_fetch на /v1/web/fetch в OmniRoute.", "interceptionLoadError": "Не удалось загрузить настройки перехвата: {error}", "interceptionSaveError": "Не удалось сохранить настройки перехвата: {error}", - "ccAliasSectionTitle": "Зеркальные ID Claude Code", - "ccAliasSectionHint": "Эти зеркала публикуют не-Claude модели под ID claude/<провайдер>/<модель>, чтобы Claude Code gateway model discovery мог их перечислить.", - "ccAliasProviderLevelLabel": "Провайдер включён", - "ccAliasModelOverridesLabel": "Переопределения моделей", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", "ccAliasModelOverrideAriaLabel": "Override for {modelId}", - "ccAliasStateInherit": "Наследовать", - "ccAliasStateOn": "Вкл", - "ccAliasStateOff": "Выкл", - "ccAliasAddModelPlaceholder": "Добавить модель…", - "ccAliasAddModelButton": "Добавить", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Дополнительные заголовки upstream", @@ -6194,7 +6209,7 @@ "galadriel": "Подключите Galadriel с помощью API-ключа.", "predibase": "Бесплатный пробный баланс $25 (срок действия 30 дней)", "chenzk": "Совместимый с OpenAI шлюз с актуальным каталогом моделей на chenzk.top.", - "freepik": "Генерация изображений с помощью Freepik Mystic API.", + "magnific": "Генерация изображений с помощью Freepik Mystic API.", "freetheai": "Бесплатный совместимый с OpenAI шлюз с поддержкой сквозной передачи моделей (passthrough).", "g4f-gemini": "Бесплатный обратный прокси g4f.space без ключа к Gemini, лимит 5 запросов в минуту.", "g4f-groq": "Бесплатный обратный прокси g4f.space без ключа к Groq, лимит 5 запросов в минуту.", @@ -6209,6 +6224,7 @@ "claude": "Подключите Claude Code с помощью существующего процесса OAuth.", "cline": "Подключите Cline с помощью существующего процесса OAuth.", "cursor": "Подключите Cursor IDE с помощью существующего процесса OAuth.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Подключите GitHub Copilot с помощью существующего процесса OAuth.", "gitlab-duo": "Приложение OAuth с областями доступа (scopes) ai_features + read_user. Настройте GITLAB_DUO_OAUTH_CLIENT_ID и, при необходимости, GITLAB_DUO_OAUTH_CLIENT_SECRET на этом экземпляре OmniRoute.", "kilocode": "Подключите Kilo Code с помощью существующего процесса OAuth.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "В периоде ожидания", "codexPoolUsed": "использовано", "codexPoolUntil": "До {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Анонимный резервный вариант", "anonymousFallbackDesc": "Когда все настроенные соединения исчерпаны (квота, кредиты или срок действия), временно используйте безключевой уровень этого провайдера. Выключите, чтобы пропустить этого провайдера вместо отправки анонимных запросов — рекомендуется, когда безключевой уровень их отклоняет (401).", "anonymousFallbackEnabled": "Анонимный резервный вариант включен для {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Настройки конечной точки сохраненной модели", "searchByModelAria": "Поиск по модели", "selectSupportedEndpoint": "Выберите хотя бы одну поддерживаемую конечную точку", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Автоматическое получение модели из upstream отключено", - "autoFetchModels": "Автоматически получать модели из upstream", - "autoFetchModelsTooltip": "Получить и кэшировать модели upstream по мере необходимости", - "autoFetchModelsEnabled": "Включен автоматический выбор модели upstream", - "overridesUpstreamModel": "Переопределяет upstream", - "overridesUpstreamModelHint": "Ваши настройки переопределяют эту модель upstream", - "autoFetchModelsToggleFailed": "Не удалось переключить автоматическую выборку модели upstream", - "autoFetchModelsPartialFailure": "Некоторые соединения обновлены, но авто-загрузка модели upstream не была изменена везде", - "resetToUpstreamDefaults": "Восстановить настройки по умолчанию upstream", - "resetToUpstreamDefaultsFailed": "Не удалось восстановить значения по умолчанию для модели upstream", - "resetToUpstreamDefaultsSuccess": "Восстановлены настройки модели по умолчанию для upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Настройки", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Навсегда помечать соединения провайдера как отключённые, если они возвращают сигнал окончательной блокировки (например, HTTP 403 'verify your account'). Это убирает их из ротации комбо.", "autoDisableThreshold": "Порог блокировки", "autoDisableThresholdDesc": "Количество подряд идущих сигналов блокировки, необходимых перед постоянным отключением.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Запрещенные ключевые слова", "customBannedSignalsDesc": "Дополнительные ключевые слова, которые вызывают обнаружение постоянной блокировки аккаунта. Встроенные ключевые слова применяются всегда.", "customBannedSignalsPlaceholder": "например, api key revoked", @@ -7210,6 +7203,7 @@ "configured": "настроено", "none": "Нет", "modelOverrideValuePlaceholder": "Числовое значение", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Добавить ключ-значение", "noModelOverrides": "Для этой модели не настроено переопределений.", "modelOverrideLoadFailed": "Не удалось загрузить переопределения модели", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Краткий CJK (文言)", "description": "Ультракраткий классический китайский стиль (доступно только для китайского языка)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Комбинации Round-robin и Random переключаются на другое подключение при каждом запросе вместо привязки всего диалога к одному подключению по хэшу первого сообщения. Оставьте выключенным, чтобы сохранить попадания в кэш промптов для многоходовых чатов. Переопределения для конкретных комбинаций имеют приоритет.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Маскирование учетных данных", "credentialRedactionDesc": "Маскировать API-ключи, токены и секреты в контексте, отправляемом провайдерам, и в ответах.", "enableCredentialRedaction": "Включить маскирование учетных данных", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Включить движок", "enableDescription": "Запускается последним в стеке (после того как RTK/Caveman очищает текст, а OmniGlyph преобразует оставшуюся часть в изображения), а также работает автономно в режиме omniglyph. Это предварительная версия, которая остается отключенной по умолчанию до завершения сквозной проверки.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Сохранено.", "saveFailed": "Не удалось сохранить.", "enableAria": "Включить движок OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "макс", "grokAutoTopUpMonth": "месяц", "grokAdditionalCredits": "Дополнительные кредиты", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Регистратор", "proxyTab": "Прокси", "budgetManagement": "Управление бюджетом", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Первый токен", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index fbd89bef06..10bf077a43 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Vizualizácia časovej osi požiadaviek", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "otvorené", "close": "zatvoriť" }, - "noResults": "Žiadne výsledky", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Žiadne výsledky" }, "webhooks": { "title": "Webhooky", @@ -1739,8 +1739,8 @@ "quotaShare": "Zdieľanie kvóty", "discovery": "Objavovanie", "freeProviderRankings": "Bezplatné rebríčky poskytovateľov", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Bezplatné úrovne", "gamification": "Gamifikácia", "leaderboard": "Rebríček", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Podpora tohto poskytovateľa bola ukončená", "riskNotice": { "title": "Pred pokračovaním", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Poskytovateľ s obmedzeniami používania — kliknutím zobrazíte podrobnosti", "oauth": "Tento poskytovateľ používa vašu oficiálnu reláciu produktu/OAuth, ktorá nie je autorizovaná na použitie ako proxy/smerovač. Neodporúčame intenzívne používanie autonómnych agentov (v štýle OpenCloud, dlhé viacstupňové toky, veľké dávky) — upstream môže reagovať obmedzením alebo zablokovaním účtu. Používajte na vlastné riziko.", "webCookie": "Tento poskytovateľ sa autentifikuje prostredníctvom súborov cookie vašej webovej relácie. Služba upstream môže reláciu kedykoľvek zneplatniť, čo si vyžiada opätovné prihlásenie. Neodporúča sa pre dlhé operácie bez dozoru. Používajte na vlastné riziko.", @@ -5107,9 +5111,9 @@ "cancel": "Zrušiť" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Zakázané", "enableProvider": "Povoliť poskytovateľa", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Preskakujem {count} existujúcich modelov", "autoSync": "Automatická synchronizácia", "autoSyncShort": "Synchronizovať", + "autoFetchModels": "Automaticky načítať upstream modely", + "autoFetchModelsTooltip": "Načítajte a uložte upstream modely, keď je to potrebné", + "autoFetchModelsEnabled": "Automatické načítanie modelu upstream je povolené", + "autoFetchModelsDisabled": "Automatické načítanie modelu upstream je zakázané", + "autoFetchModelsToggleFailed": "Nepodarilo sa prepnúť automatické získavanie modelu upstream", + "autoFetchModelsPartialFailure": "Niektoré pripojenia boli aktualizované, ale automatické načítanie modelu upstream nebolo zmenené všade", + "overridesUpstreamModel": "Prepisuje upstream", + "overridesUpstreamModelHint": "Vaše nastavenia prepisujú tento upstream model", + "resetToUpstreamDefaults": "Obnoviť predvolené nastavenia upstream", + "resetToUpstreamDefaultsSuccess": "Obnovené predvolené nastavenia upstream modelu", + "resetToUpstreamDefaultsFailed": "Obnovenie predvolených nastavení modelu upstream zlyhalo", "autoSyncTooltip": "Automaticky obnovovať zoznam modelov každých 24 hodín (konfigurovateľné cez MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatická synchronizácia povolená – modely sa budú pravidelne obnovovať", "autoSyncDisabled": "Automatická synchronizácia je zakázaná", @@ -5439,17 +5454,17 @@ "interceptionLoadError": "Nepodarilo sa načítať nastavenia zachytávania: {error}", "interceptionSaveError": "Nepodarilo sa uložiť nastavenia zachytávania: {error}", "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "Inzerujte modely tohto poskytovateľa pod claude/<provider>/<model> zrkadlovými ID, aby mohol modelový objav brány Claude Code ich zoznamovať. Predvolene vypnuté — povolením sa zdvojnásobia záznamy v katalógu pre všetkých klientov.", - "ccAliasProviderLevelLabel": "Predvolený poskytovateľ", - "ccAliasModelOverridesLabel": "Pre každú modelovú výnimku", - "ccAliasModelOverrideAriaLabel": "Prepis pre {modelId}", - "ccAliasStateInherit": "Dedičstvo", - "ccAliasStateOn": "Na", - "ccAliasStateOff": "Vypnuté", - "ccAliasAddModelPlaceholder": "Model id (napr. gpt-4o)", - "ccAliasAddModelButton": "Pridať prepis", - "ccAliasLoadError": "Nepodarilo sa načítať nastavenia discovery-alias: {error}", - "ccAliasSaveError": "Nepodarilo sa uložiť nastavenie discovery-alias: {error}", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Pripojte Galadriel pomocou API kľúča.", "predibase": "Bezplatný skúšobný kredit 25 $ (platnosť 30 dní)", "chenzk": "Brána kompatibilná s OpenAI so živým katalógom modelov na chenzk.top.", - "freepik": "Generujte obrázky pomocou Mystic API od Freepik.", + "magnific": "Generujte obrázky pomocou Mystic API od Freepik.", "freetheai": "Bezplatná brána kompatibilná s OpenAI s podporou prenosu modelov (passthrough).", "g4f-gemini": "Bezplatný reverzný proxy server g4f.space bez kľúča pre Gemini, obmedzený na 5 požiadaviek za minútu.", "g4f-groq": "Bezplatný reverzný proxy server g4f.space bez kľúča pre Groq, obmedzený na 5 požiadaviek za minútu.", @@ -6209,6 +6224,7 @@ "claude": "Pripojte Claude Code pomocou existujúceho toku OAuth.", "cline": "Pripojte Cline pomocou existujúceho toku OAuth.", "cursor": "Pripojte Cursor IDE pomocou existujúceho toku OAuth.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Pripojte GitHub Copilot pomocou existujúceho toku OAuth.", "gitlab-duo": "Aplikácia OAuth s rozsahmi (scopes) ai_features + read_user. Nakonfigurujte GITLAB_DUO_OAUTH_CLIENT_ID a voliteľne GITLAB_DUO_OAUTH_CLIENT_SECRET na tejto inštancii OmniRoute.", "kilocode": "Pripojte Kilo Code pomocou existujúceho toku OAuth.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "V čakacej lehote", "codexPoolUsed": "využité", "codexPoolUntil": "Do {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonymný záložný systém", "anonymousFallbackDesc": "Keď sú všetky nakonfigurované pripojenia vyčerpané (kvóta, kredity alebo vypršanie platnosti), dočasne použite bezkľúčovú úroveň tohto poskytovateľa. Vypnite, aby ste preskočili tohto poskytovateľa namiesto odosielania anonymných požiadaviek — odporúča sa, keď bezkľúčová úroveň ich odmieta (401).", "anonymousFallbackEnabled": "Anonymný záložný režim povolený pre {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Nastavenia koncového bodu uloženého modelu", "searchByModelAria": "Hľadať podľa modelu", "selectSupportedEndpoint": "Vyberte aspoň jeden podporovaný koncový bod", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsEnabled": "Automatické načítanie modelu upstream je povolené", - "autoFetchModelsDisabled": "Automatické načítanie modelu upstream je zakázané", - "autoFetchModelsTooltip": "Načítajte a uložte upstream modely, keď je to potrebné", - "autoFetchModels": "Automaticky načítať upstream modely", - "overridesUpstreamModel": "Prepisuje upstream", - "autoFetchModelsToggleFailed": "Nepodarilo sa prepnúť automatické získavanie modelu upstream", - "overridesUpstreamModelHint": "Vaše nastavenia prepisujú tento upstream model", - "autoFetchModelsPartialFailure": "Niektoré pripojenia boli aktualizované, ale automatické načítanie modelu upstream nebolo zmenené všade", - "resetToUpstreamDefaultsSuccess": "Obnovené predvolené nastavenia upstream modelu", - "resetToUpstreamDefaults": "Obnoviť predvolené nastavenia upstream", - "resetToUpstreamDefaultsFailed": "Obnovenie predvolených nastavení modelu upstream zlyhalo" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Nastavenia", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Zakázané kľúčové slová", "customBannedSignalsDesc": "Ďalšie kľúčové slová, ktoré spúšťajú detekciu trvalého zablokovania účtu. Vstavané kľúčové slová platia vždy.", "customBannedSignalsPlaceholder": "napr. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "nakonfigurované", "none": "Žiadne", "modelOverrideValuePlaceholder": "Číselná hodnota", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Pridať kľúč – hodnotu", "noModelOverrides": "Pre tento model nie sú nakonfigurované žiadne prepísania.", "modelOverrideLoadFailed": "Nepodarilo sa načítať prepísania modelov", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Stručné CJK (文言)", "description": "Klasický čínsky ultra stručný štýl (dostupný len pre čínštinu)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Kombinácie round-robin a náhodného výberu sa pri každej požiadavke prepnú na iné pripojenie namiesto toho, aby celú konverzáciu priradili k jednému pripojeniu na základe hašu prvej správy. Ponechajte vypnuté, ak chcete zachovať zásahy do vyrovnávacej pamäte promptov (prompt-cache) pre viacúrovňové chaty. Prepísania pre jednotlivé kombinácie majú prednosť.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Maskovanie prihlasovacích údajov", "credentialRedactionDesc": "Maskovať API kľúče, tokeny a tajné kľúče v kontexte odosielanom poskytovateľom a v odpovediach.", "enableCredentialRedaction": "Enable credential redaction", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Povoliť engine", "enableDescription": "Spúšťa sa ako posledný v stacku (po tom, čo RTK/Caveman vyčistí text, OmniGlyph skonvertuje zvyšok na obrázky) a funguje aj samostatne v režime omniglyph. Toto je predbežná verzia a predvolene zostáva vypnutá, kým sa nedokončí end-to-end validácia.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Uložené.", "saveFailed": "Nepodarilo sa uložiť.", "enableAria": "Povoliť engine OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "mesiac", "grokAdditionalCredits": "Ďalšie kredity", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Správa rozpočtu", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Prvý token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 75b165386a..46f53972d7 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Visuell begäran tidslinje", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "öppna", "close": "stäng" }, - "noResults": "Inga resultat", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Inga resultat" }, "webhooks": { "title": "Webhooks", @@ -1739,8 +1739,8 @@ "quotaShare": "Kvotandel", "discovery": "Upptäckt", "freeProviderRankings": "Rankning av gratisleverantörer", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Gratisnivåer", "gamification": "Spelifiering", "leaderboard": "Topplista", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Denna leverantör har fasats ut", "riskNotice": { "title": "Innan du fortsätter", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Leverantör med användningsförbehåll — klicka för detaljer", "oauth": "Denna leverantör använder din officiella produktsession/OAuth, vilket inte är godkänt för proxy-/routeranvändning. Vi rekommenderar inte intensiv användning av autonoma agenter (OpenCloud-stil, långa flerstegsflöden, stora batcher) — uppströmsleverantören kan reagera genom att begränsa eller stänga av kontot. Används på egen risk.", "webCookie": "Denna leverantör autentiserar via dina webbsessionscookies. Uppströmstjänsten kan ogiltigförklara sessionen när som helst, vilket kräver att du loggar in igen. Rekommenderas inte för långa obevakade körningar. Används på egen risk.", @@ -5107,9 +5111,9 @@ "cancel": "Avbryt" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Inaktiverad", "enableProvider": "Aktivera leverantör", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Hoppar över {count} befintliga modeller", "autoSync": "Automatisk synkronisering", "autoSyncShort": "Synkronisera", + "autoFetchModels": "Automatiskt hämta upstream-modeller", + "autoFetchModelsTooltip": "Hämta och cacha upstream-modeller vid behov", + "autoFetchModelsEnabled": "Automatisk hämtning av upstream-modell aktiverad", + "autoFetchModelsDisabled": "Automatisk hämtning av upstream-modell inaktiverad", + "autoFetchModelsToggleFailed": "Misslyckades med att växla upstream-modellens automatisk hämtning", + "autoFetchModelsPartialFailure": "Vissa anslutningar har uppdaterats, men upstream-modellens automatisk hämtning ändrades inte överallt", + "overridesUpstreamModel": "Överskrider upstream", + "overridesUpstreamModelHint": "Dina inställningar åsidosätter denna upstream-modell", + "resetToUpstreamDefaults": "Återställ upstream-standarder", + "resetToUpstreamDefaultsSuccess": "Återställda standardinställningar för upstream-modellen", + "resetToUpstreamDefaultsFailed": "Misslyckades med att återställa standardinställningar för upstream-modellen", "autoSyncTooltip": "Uppdatera modelllistan automatiskt var 24:e timme (konfigurerbar via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Automatisk synkronisering aktiverad — modeller uppdateras regelbundet", "autoSyncDisabled": "Automatisk synkronisering inaktiverad", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Skriv om inbyggda web_fetch-verktygsanrop till OmniRoutes /v1/web/fetch.", "interceptionLoadError": "Kunde inte läsa in inställningar för interception: {error}", "interceptionSaveError": "Kunde inte spara inställningar för interception: {error}", - "ccAliasSectionTitle": "Exponera i Claude Code (claude/…)", - "ccAliasSectionHint": "Reklamera denna leverantörs modeller under claude/<provider>/<model> spegel-id så att Claude Codes gateway-modellupptäckten kan lista dem. Avstängd som standard — att aktivera detta dubblar katalogposterna för alla klienter.", - "ccAliasProviderLevelLabel": "Leverantör standard", - "ccAliasModelOverridesLabel": "Per-modell överskrivningar", - "ccAliasModelOverrideAriaLabel": "Överskrivning för {modelId}", - "ccAliasStateInherit": "Ärv", - "ccAliasStateOn": "På", - "ccAliasStateOff": "Av", - "ccAliasAddModelPlaceholder": "Modell-id (t.ex. gpt-4o)", - "ccAliasAddModelButton": "Lägg till överskrivning", - "ccAliasLoadError": "Misslyckades med att ladda discovery-alias-inställningar: {error}", - "ccAliasSaveError": "Misslyckades med att spara inställningen för discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Anslut Galadriel med en API-nyckel.", "predibase": "$25 i gratis provkrediter (30 dagars giltighet)", "chenzk": "OpenAI-kompatibel gateway med en live-modellkatalog på chenzk.top.", - "freepik": "Generera bilder med Freepiks Mystic API.", + "magnific": "Generera bilder med Freepiks Mystic API.", "freetheai": "Gratis OpenAI-kompatibel gateway med stöd för passthrough-modeller.", "g4f-gemini": "Gratis nyckelfri g4f.space reverse proxy till Gemini, begränsad till 5 anrop per minut.", "g4f-groq": "Gratis nyckelfri g4f.space reverse proxy till Groq, begränsad till 5 anrop per minut.", @@ -6209,6 +6224,7 @@ "claude": "Anslut Claude Code med det befintliga OAuth-flödet.", "cline": "Anslut Cline med det befintliga OAuth-flödet.", "cursor": "Anslut Cursor IDE med det befintliga OAuth-flödet.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Anslut GitHub Copilot med det befintliga OAuth-flödet.", "gitlab-duo": "OAuth-applikation med ai_features + read_user-omfång. Konfigurera GITLAB_DUO_OAUTH_CLIENT_ID och valfritt GITLAB_DUO_OAUTH_CLIENT_SECRET på denna OmniRoute-instans.", "kilocode": "Anslut Kilo Code med det befintliga OAuth-flödet.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "I vänteperiod", "codexPoolUsed": "använt", "codexPoolUntil": "Till {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonym fallback", "anonymousFallbackDesc": "När alla konfigurerade anslutningar är uttömda (kvot, krediter eller utgång), använd tillfälligt denna leverantörs nyckellösa nivå. Stäng av för att hoppa över denna leverantör istället för att skicka anonyma förfrågningar — rekommenderas när den nyckellösa nivån avvisar dem (401).", "anonymousFallbackEnabled": "Anonym fallback aktiverad för {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Inställningar för sparad modellslutpunkt", "searchByModelAria": "Sök efter modell", "selectSupportedEndpoint": "Välj minst en stödd slutpunkt", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Automatiskt hämta upstream-modeller", - "autoFetchModelsDisabled": "Automatisk hämtning av upstream-modell inaktiverad", - "autoFetchModelsEnabled": "Automatisk hämtning av upstream-modell aktiverad", - "autoFetchModelsTooltip": "Hämta och cacha upstream-modeller vid behov", - "overridesUpstreamModel": "Överskrider upstream", - "autoFetchModelsToggleFailed": "Misslyckades med att växla upstream-modellens automatisk hämtning", - "autoFetchModelsPartialFailure": "Vissa anslutningar har uppdaterats, men upstream-modellens automatisk hämtning ändrades inte överallt", - "overridesUpstreamModelHint": "Dina inställningar åsidosätter denna upstream-modell", - "resetToUpstreamDefaults": "Återställ upstream-standarder", - "resetToUpstreamDefaultsSuccess": "Återställda standardinställningar för upstream-modellen", - "resetToUpstreamDefaultsFailed": "Misslyckades med att återställa standardinställningar för upstream-modellen" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Inställningar", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Spärrade nyckelord", "customBannedSignalsDesc": "Ytterligare nyckelord som utlöser upptäckt av permanent kontoavstängning. Inbyggda nyckelord gäller alltid.", "customBannedSignalsPlaceholder": "t.ex. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "konfigurerad", "none": "Ingen", "modelOverrideValuePlaceholder": "Numeriskt värde", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Lägg till nyckelvärde", "noModelOverrides": "Inga åsidosättningar har konfigurerats för denna modell.", "modelOverrideLoadFailed": "Det gick inte att läsa in modellåsidosättningar", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Kortfattad CJK (文言)", "description": "Klassisk kinesisk ultrakortfattad stil (endast tillgänglig för kinesiska)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin- och slumpmässiga kombinationer roterar till en annan anslutning vid varje anrop istället för att fästa en hel konversation vid en anslutning baserat på hashen för det första meddelandet. Lämna inaktiverat för att bevara prompt-cache-träffar för flerstegschattar. Inställningar per kombination har företräde.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Maskering av autentiseringsuppgifter", "credentialRedactionDesc": "Maskera API-nycklar, tokens och hemligheter från kontext som skickas till leverantörer och från svar.", "enableCredentialRedaction": "Aktivera maskering av autentiseringsuppgifter", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Aktivera motorn", "enableDescription": "Körs sist i stacken (efter att RTK/Caveman rensar texten konverterar OmniGlyph resten till bilder) och körs även fristående via omniglyph-läge. Detta är en förhandsgranskning och förblir inaktiverad som standard tills end-to-end-valideringen är slutförd.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Sparat.", "saveFailed": "Kunde inte spara.", "enableAria": "Aktivera OmniGlyph-motorn", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "månad", "grokAdditionalCredits": "Ytterligare Krediter", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budgethantering", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Första token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 15f8f0e290..12d19b28c1 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Muda wa ombi la kuona", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "fungua", "close": "funga" }, - "noResults": "Hakuna matokeo", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Hakuna matokeo" }, "webhooks": { "title": "Viboko vya mtandao", @@ -1739,8 +1739,8 @@ "quotaShare": "Mgao wa Quota", "discovery": "Ugunduzi", "freeProviderRankings": "Nafasi za Watoa Huduma Bila Malipo", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Viwango vya Bila Malipo", "gamification": "Uchezeshaji", "leaderboard": "Ubao wa Wanaoongoza", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Mtoa huduma huyu ameacha kutumika", "riskNotice": { "title": "Kabla ya kuendelea", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Mtoa huduma aliye na tahadhari za matumizi — bofya kwa maelezo zaidi", "oauth": "Mtoa huduma huyu anatumia kipindi chako rasmi cha bidhaa/OAuth, ambacho hakijaidhinishwa kwa matumizi ya proksi/ruta. Hatupendekezi matumizi makubwa ya ejenti inayojitegemea (mtindo wa OpenCloud, mtiririko mrefu wa hatua nyingi, makundi makubwa) — upstream inaweza kuchukua hatua kwa kuzuia au kupiga marufuku akaunti. Tumia kwa hatari yako mwenyewe.", "webCookie": "Mtoa huduma huyu anathibitisha kupitia kuki za kipindi chako cha wavuti. Huduma ya upstream inaweza kubatilisha kipindi wakati wowote, ikikuhitaji uingie tena. Haipendekezwi kwa shughuli ndefu zisizosimamiwa. Tumia kwa hatari yako mwenyewe.", @@ -5107,9 +5111,9 @@ "cancel": "Ghairi" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "Pata modeli za upstream kiotomatiki", + "autoFetchModelsTooltip": "Pata na kuhifadhi mifano ya juu inapohitajika", + "autoFetchModelsEnabled": "Mfano wa upstream auto-fetch umewezeshwa", + "autoFetchModelsDisabled": "Mfano wa upstream auto-fetch umezimwa", + "autoFetchModelsToggleFailed": "Imeshindikana kubadilisha hali ya upakuaji wa mfano wa juu.", + "autoFetchModelsPartialFailure": "Baadhi ya muunganisho yameboreshwa, lakini mfano wa juu wa auto-fetch haukubadilishwa kila mahali", + "overridesUpstreamModel": "Inazidi mwelekeo wa juu", + "overridesUpstreamModelHint": "Mipangilio yako inakataa mfano huu wa juu", + "resetToUpstreamDefaults": "Rejesha mipangilio ya msingi ya upstream", + "resetToUpstreamDefaultsSuccess": "Imerejeshwa mipangilio ya mfano wa upstream", + "resetToUpstreamDefaultsFailed": "Imeshindikana kurejesha mipangilio ya msingi ya upstream", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Andika upya simu za zana asili za web_fetch kwenda kwenye /v1/web/fetch ya OmniRoute.", "interceptionLoadError": "Imeshindwa kupakia mipangilio ya uzuiaji: {error}", "interceptionSaveError": "Imeshindwa kuhifadhi mipangilio ya uzuiaji: {error}", - "ccAliasSectionTitle": "Fichua katika Claude Code (claude/…)", - "ccAliasSectionHint": "Tangaza mifano ya mtoa huduma huyu chini ya claude/<provider>/<model> vitambulisho vya kioo ili kugundua mifano ya lango la Claude Code. Imezimwa kwa default — kuwezesha hii kunaongeza mara mbili orodha za katalogi kwa wateja wote.", - "ccAliasProviderLevelLabel": "Mtoa huduma wa kawaida", - "ccAliasModelOverridesLabel": "Mabadiliko ya kila mfano", - "ccAliasModelOverrideAriaLabel": "Kuzidisha kwa {modelId}", - "ccAliasStateInherit": "Rithi", - "ccAliasStateOn": "Juu", - "ccAliasStateOff": "Zimezimwa", - "ccAliasAddModelPlaceholder": "Kitambulisho cha mfano (mfano: gpt-4o)", - "ccAliasAddModelButton": "Ongeza urekebishaji", - "ccAliasLoadError": "Imeshindikana kupakia mipangilio ya discovery-alias: {error}", - "ccAliasSaveError": "Imeshindikana kuhifadhi mipangilio ya discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Unganisha Galadriel kwa ufunguo wa API.", "predibase": "Salio la majaribio ya bure la $25 (uhalali wa siku 30)", "chenzk": "Lango linalooana na OpenAI lenye orodha ya miundo ya moja kwa moja kwenye chenzk.top.", - "freepik": "Zalisha picha kwa kutumia Mystic API ya Freepik.", + "magnific": "Zalisha picha kwa kutumia Mystic API ya Freepik.", "freetheai": "Lango la bure linalooana na OpenAI lenye usaidizi wa miundo ya passthrough.", "g4f-gemini": "Reverse proxy ya bure isiyo na ufunguo ya g4f.space kwenda Gemini, yenye kikomo cha maombi 5 kwa dakika.", "g4f-groq": "Reverse proxy ya bure isiyo na ufunguo ya g4f.space kwenda Groq, yenye kikomo cha maombi 5 kwa dakika.", @@ -6209,6 +6224,7 @@ "claude": "Unganisha Claude Code kwa mtiririko uliopo wa OAuth.", "cline": "Unganisha Cline kwa mtiririko uliopo wa OAuth.", "cursor": "Unganisha Cursor IDE kwa mtiririko uliopo wa OAuth.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Unganisha GitHub Copilot kwa mtiririko uliopo wa OAuth.", "gitlab-duo": "Programu ya OAuth yenye upeo wa ai_features + read_user. Sanidi GITLAB_DUO_OAUTH_CLIENT_ID na kwa hiari GITLAB_DUO_OAUTH_CLIENT_SECRET kwenye instansi hii ya OmniRoute.", "kilocode": "Unganisha Kilo Code kwa mtiririko uliopo wa OAuth.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Katika kipindi cha kusubiri", "codexPoolUsed": "imetumika", "codexPoolUntil": "Hadi {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Kurejea kwa Kijakazi", "anonymousFallbackDesc": "Wakati muunganisho wote uliowekwa umepita (kikomo, mikopo, au muda wa kumalizika), tumia muda huu kiwango kisicho na funguo cha mtoa huduma huyu. Zima ili kupuuza mtoa huduma huyu badala ya kutuma maombi yasiyo na utambulisho — inapendekezwa wakati kiwango kisicho na funguo kinapokataa maombi hayo (401).", "anonymousFallbackEnabled": "Fallback isiyojulikana imewezeshwa kwa {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Mipangilio ya mwisho wa mfano uliohifadhiwa", "searchByModelAria": "Tafuta kwa mfano", "selectSupportedEndpoint": "Chagua angalau kiunganishi kimoja kinachoungwa mkono", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsEnabled": "Mfano wa upstream auto-fetch umewezeshwa", - "autoFetchModelsDisabled": "Mfano wa upstream auto-fetch umezimwa", - "autoFetchModels": "Pata modeli za upstream kiotomatiki", - "autoFetchModelsTooltip": "Pata na kuhifadhi mifano ya juu inapohitajika", - "autoFetchModelsToggleFailed": "Imeshindikana kubadilisha hali ya upakuaji wa mfano wa juu.", - "autoFetchModelsPartialFailure": "Baadhi ya muunganisho yameboreshwa, lakini mfano wa juu wa auto-fetch haukubadilishwa kila mahali", - "overridesUpstreamModelHint": "Mipangilio yako inakataa mfano huu wa juu", - "overridesUpstreamModel": "Inazidi mwelekeo wa juu", - "resetToUpstreamDefaults": "Rejesha mipangilio ya msingi ya upstream", - "resetToUpstreamDefaultsSuccess": "Imerejeshwa mipangilio ya mfano wa upstream", - "resetToUpstreamDefaultsFailed": "Imeshindikana kurejesha mipangilio ya msingi ya upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Maneno Muhimu Yaliyopigwa Marufuku", "customBannedSignalsDesc": "Maneno muhimu ya ziada yanayosababisha ugunduzi wa kupigwa marufuku kwa akaunti kabisa. Maneno muhimu yaliyojengwa ndani hutumika kila wakati.", "customBannedSignalsPlaceholder": "k.m. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "imesanidiwa", "none": "Hakuna", "modelOverrideValuePlaceholder": "Thamani ya nambari", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Ongeza thamani ya ufunguo", "noModelOverrides": "Hakuna ubatilishaji uliosanidiwa kwa modeli hii.", "modelOverrideLoadFailed": "Imeshindwa kupakia ubatilishaji wa modeli", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK Fupi (文言)", "description": "Mtindo mfupi zaidi wa Kichina cha Kale (inapatikana kwa Kichina pekee)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Mchanganyiko wa round-robin na bila mpangilio huzunguka hadi kwenye muunganisho tofauti kwa kila ombi badala ya kubandika mazungumzo yote kwenye muunganisho mmoja kwa heshi ya ujumbe wa kwanza. Acha ikiwa imezimwa ili kuhifadhi matokeo ya prompt-cache kwa mazungumzo ya zamu nyingi. Ubatilishaji wa kila mchanganyiko una kipaumbele.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Ufichaji wa Vitambulisho", "credentialRedactionDesc": "Ficha funguo za API, tokeni, na siri kutoka kwa muktadha uliotumwa kwa watoa huduma na kutoka kwa majibu.", "enableCredentialRedaction": "Washa ufichaji wa vitambulisho", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Wezesha injini", "enableDescription": "Hufanya kazi mwisho kwenye mrundikano (baada ya RTK/Caveman kusafisha maandishi, OmniGlyph hubadilisha yaliyosalia kuwa picha) na pia hufanya kazi pekee kupitia hali ya omniglyph. Hii ni hakiki na inabaki imezimwa kwa chaguomsingi hadi uthibitishaji wa mwisho hadi mwisho ukamilike.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Imehifadhiwa.", "saveFailed": "Imeshindwa kuhifadhi.", "enableAria": "Wezesha injini ya OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "max", "grokAutoTopUpMonth": "mwezi", "grokAdditionalCredits": "Mikopo Ya Ziada", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Tokeni ya Kwanza", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index d9bb96f0f4..2e80cf7f22 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "காட்சி கோரிக்கை காலக்கெடு", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "திறக்கவும்", "close": "மூடு" }, - "noResults": "எந்த முடிவுகளும் இல்லை", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "எந்த முடிவுகளும் இல்லை" }, "webhooks": { "title": "வெப்ஹூக்ஸ்", @@ -1739,8 +1739,8 @@ "quotaShare": "ஒதுக்கீட்டுப் பகிர்வு", "discovery": "கண்டறிதல்", "freeProviderRankings": "இலவச வழங்குநர் தரவரிசை", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "இலவச அடுக்குகள்", "gamification": "கேமிஃபிகேஷன்", "leaderboard": "லீடர்போர்டு", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "இந்த வழங்குநர் நிராகரிக்கப்பட்டார்", "riskNotice": { "title": "தொடர்வதற்கு முன்", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "பயன்பாட்டு எச்சரிக்கைகளைக் கொண்ட வழங்குநர் — விவரங்களுக்கு கிளிக் செய்யவும்", "oauth": "இந்த வழங்குநர் உங்களது அதிகாரப்பூர்வ தயாரிப்பு அமர்வு/OAuth ஐப் பயன்படுத்துகிறார், இது ப்ராக்ஸி/ரவுட்டர் பயன்பாட்டிற்கு அங்கீகரிக்கப்படவில்லை. தீவிரமான தன்னாட்சி முகவர் பயன்பாட்டை (OpenCloud-பாணி, நீண்ட பல-படி ஓட்டங்கள், பெரிய தொகுதிகள்) நாங்கள் பரிந்துரைக்கவில்லை — அப்ஸ்ட்ரீம் கணக்கைக் கட்டுப்படுத்துவதன் மூலமோ அல்லது தடை செய்வதன் மூலமோ எதிர்வினையாற்றலாம். உங்கள் சொந்த பொறுப்பில் பயன்படுத்தவும்.", "webCookie": "இந்த வழங்குநர் உங்கள் வலை அமர்வு குக்கீகள் மூலம் அங்கீகரிக்கிறார். அப்ஸ்ட்ரீம் சேவை எந்த நேரத்திலும் அமர்வை செல்லாததாக்கலாம், இதனால் நீங்கள் மீண்டும் உள்நுழைய வேண்டியிருக்கும். நீண்ட கவனிக்கப்படாத செயல்பாடுகளுக்கு பரிந்துரைக்கப்படவில்லை. உங்கள் சொந்த பொறுப்பில் பயன்படுத்தவும்.", @@ -5107,9 +5111,9 @@ "cancel": "ரத்துசெய்" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "உயர்தர மாதிரிகளை தானாகப் பெறவும்", + "autoFetchModelsTooltip": "தேவையான போது மேல்நிலை மாதிரிகளை பெறவும் மற்றும் கச்சே செய்யவும்", + "autoFetchModelsEnabled": "மேல்நிலை மாதிரி தானாகப் பெறுதல் செயல்படுத்தப்பட்டது", + "autoFetchModelsDisabled": "மேல்நிலை மாதிரி தானாகப் பெறுதல் முடக்கப்பட்டது", + "autoFetchModelsToggleFailed": "மேல்தர மாதிரி தானாகப் பெறுதலை மாற்ற முடியவில்லை", + "autoFetchModelsPartialFailure": "சில இணைப்புகள் புதுப்பிக்கப்பட்டன, ஆனால் மேல்மட்ட மாதிரி தானாகப் பெறுதல் எங்கும் மாற்றப்படவில்லை", + "overridesUpstreamModel": "மேல்நிலை மாற்றங்கள்", + "overridesUpstreamModelHint": "உங்கள் அமைப்புகள் இந்த மேல்மட்ட மாதிரியை மீறுகின்றன", + "resetToUpstreamDefaults": "முதன்மை இயல்புகளை மீட்டமைக்கவும்", + "resetToUpstreamDefaultsSuccess": "மீட்டமைக்கப்பட்ட மேல்நிலை மாதிரி இயல்புகள்", + "resetToUpstreamDefaultsFailed": "மேல்நிலை மாதிரி இயல்புகளை மீட்டெடுக்க முடியவில்லை", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "சொந்த web_fetch கருவி அழைப்புகளை OmniRoute இன் /v1/web/fetch க்கு மீண்டும் எழுதவும்.", "interceptionLoadError": "இடைமறிப்பு அமைப்புகளை ஏற்றுவதில் தோல்வி: {error}", "interceptionSaveError": "இடைமறிப்பு அமைப்புகளைச் சேமிப்பதில் தோல்வி: {error}", - "ccAliasSectionTitle": "Claude Code-ல் வெளிப்படுத்தவும் (claude/…)", - "ccAliasSectionHint": "இந்த வழங்குநரின் மாதிரிகளை claude/<provider>/<model> மிரர் அடையாளங்களின் கீழ் விளம்பரம் செய்யவும், எனவே Claude Code இன் கேட்வே மாதிரி கண்டுபிடிப்பு அவற்றைப் பட்டியலிடலாம். இயல்பாக அண்மையில் отключено — இதை இயக்குவது அனைத்து கிளையன்டுகளுக்கான பட்டியல் பதிவுகளை இரட்டிப்பாக்குகிறது.", - "ccAliasProviderLevelLabel": "முதன்மை வழங்குநர்", - "ccAliasModelOverridesLabel": "மாதிரி அடிப்படையில் மீறல்கள்", - "ccAliasModelOverrideAriaLabel": "{modelId} க்கான மீறல்", - "ccAliasStateInherit": "மரபு", - "ccAliasStateOn": "இல்", - "ccAliasStateOff": "ஆஃப்", - "ccAliasAddModelPlaceholder": "மாதிரி ஐடி (எடுத்துக்காட்டு: gpt-4o)", - "ccAliasAddModelButton": "மீட்டமைப்பு சேர்க்கவும்", - "ccAliasLoadError": "கண்டுபிடிப்பு-அலியாஸ் அமைப்புகளை ஏற்றுவதில் தோல்வி: {error}", - "ccAliasSaveError": "கண்டுபிடிப்பு-அலியாஸ் அமைப்பை சேமிக்க முடியவில்லை: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "API key மூலம் Galadriel-ஐ இணைக்கவும்.", "predibase": "$25 இலவச சோதனை கிரெடிட்கள் (30 நாள் செல்லுபடியாகும்)", "chenzk": "chenzk.top இல் நேரடி மாடல் பட்டியலுடன் கூடிய OpenAI-இணக்கமான gateway.", - "freepik": "Freepik-இன் Mystic API மூலம் படங்களை உருவாக்கவும்.", + "magnific": "Freepik-இன் Mystic API மூலம் படங்களை உருவாக்கவும்.", "freetheai": "passthrough மாடல் ஆதரவுடன் கூடிய இலவச OpenAI-இணக்கமான gateway.", "g4f-gemini": "Gemini-க்கான இலவச key இல்லாத g4f.space reverse proxy, நிமிடத்திற்கு 5 கோரிக்கைகள் என வரம்பிடப்பட்டுள்ளது.", "g4f-groq": "Groq-க்கான இலவச key இல்லாத g4f.space reverse proxy, நிமிடத்திற்கு 5 கோரிக்கைகள் என வரம்பிடப்பட்டுள்ளது.", @@ -6209,6 +6224,7 @@ "claude": "ஏற்கனவே உள்ள OAuth flow மூலம் Claude Code-ஐ இணைக்கவும்.", "cline": "ஏற்கனவே உள்ள OAuth flow மூலம் Cline-ஐ இணைக்கவும்.", "cursor": "ஏற்கனவே உள்ள OAuth flow மூலம் Cursor IDE-ஐ இணைக்கவும்.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "ஏற்கனவே உள்ள OAuth flow மூலம் GitHub Copilot-ஐ இணைக்கவும்.", "gitlab-duo": "ai_features + read_user scopes கொண்ட OAuth பயன்பாடு. இந்த OmniRoute instance-இல் GITLAB_DUO_OAUTH_CLIENT_ID மற்றும் விருப்பத்தேர்வாக GITLAB_DUO_OAUTH_CLIENT_SECRET-ஐ உள்ளமைக்கவும்.", "kilocode": "ஏற்கனவே உள்ள OAuth flow மூலம் Kilo Code-ஐ இணைக்கவும்.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "காத்திருப்பு காலத்தில் உள்ளது", "codexPoolUsed": "பயன்படுத்தப்பட்டது", "codexPoolUntil": "{value} வரை", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "அறியப்படாத மாற்று", "anonymousFallbackDesc": "எல்லா கட்டமைக்கப்பட்ட இணைப்புகள் முடிந்தால் (கோட்டா, கிரெடிட்கள், அல்லது காலாவதி), இந்த வழங்குநரின் விசையில்லா நிலையை தற்காலிகமாக பயன்படுத்தவும். இந்த வழங்குநரை தவிர்க்க மாறி அனான்மா கோரிக்கைகளை அனுப்பாமல் выключить செய்யவும் — விசையில்லா நிலை அவற்றை நிராகரிக்கும் போது (401) பரிந்துரைக்கப்படுகிறது.", "anonymousFallbackEnabled": "{provider} க்கான அங்கீகாரம் இல்லாத மாற்று செயல்படுத்தப்பட்டது", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "சேமிக்கப்பட்ட மாதிரி முடிவுறுப்பு அமைப்புகள்", "searchByModelAria": "மாதிரியில் தேடு", "selectSupportedEndpoint": "குறைந்தது ஒரு ஆதரிக்கப்படும் முடிவுகளைத் தேர்ந்தெடுக்கவும்", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsTooltip": "தேவையான போது மேல்நிலை மாதிரிகளை பெறவும் மற்றும் கச்சே செய்யவும்", - "autoFetchModelsDisabled": "மேல்நிலை மாதிரி தானாகப் பெறுதல் முடக்கப்பட்டது", - "autoFetchModels": "உயர்தர மாதிரிகளை தானாகப் பெறவும்", - "autoFetchModelsEnabled": "மேல்நிலை மாதிரி தானாகப் பெறுதல் செயல்படுத்தப்பட்டது", - "overridesUpstreamModel": "மேல்நிலை மாற்றங்கள்", - "autoFetchModelsPartialFailure": "சில இணைப்புகள் புதுப்பிக்கப்பட்டன, ஆனால் மேல்மட்ட மாதிரி தானாகப் பெறுதல் எங்கும் மாற்றப்படவில்லை", - "autoFetchModelsToggleFailed": "மேல்தர மாதிரி தானாகப் பெறுதலை மாற்ற முடியவில்லை", - "overridesUpstreamModelHint": "உங்கள் அமைப்புகள் இந்த மேல்மட்ட மாதிரியை மீறுகின்றன", - "resetToUpstreamDefaults": "முதன்மை இயல்புகளை மீட்டமைக்கவும்", - "resetToUpstreamDefaultsSuccess": "மீட்டமைக்கப்பட்ட மேல்நிலை மாதிரி இயல்புகள்", - "resetToUpstreamDefaultsFailed": "மேல்நிலை மாதிரி இயல்புகளை மீட்டெடுக்க முடியவில்லை" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "தடைசெய்யப்பட்ட முக்கிய வார்த்தைகள்", "customBannedSignalsDesc": "நிரந்தரக் கணக்குத் தடை கண்டறிதலைத் தூண்டும் கூடுதல் முக்கிய வார்த்தைகள். உள்ளமைக்கப்பட்ட முக்கிய வார்த்தைகள் எப்போதும் பொருந்தும்.", "customBannedSignalsPlaceholder": "எ.கா. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "கட்டமைக்கப்பட்டது", "none": "ஏதுமில்லை", "modelOverrideValuePlaceholder": "எண் மதிப்பு", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "விசை மதிப்பைச் சேர்", "noModelOverrides": "இந்த மாதிரிக்கு மேலெழுதல்கள் எதுவும் கட்டமைக்கப்படவில்லை.", "modelOverrideLoadFailed": "மாதிரி மேலெழுதல்களை ஏற்றுவதில் தோல்வி", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "சுருக்கமான CJK (文言)", "description": "செம்மொழி-சீன மிகச் சுருக்கமான நடை (சீன மொழிக்கு மட்டுமே கிடைக்கும்)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "முதல்-செய்தி ஹாஷ் மூலம் முழு உரையாடலையும் ஒரே இணைப்பில் நிலைநிறுத்துவதற்குப் பதிலாக, Round-robin மற்றும் random சேர்க்கைகள் ஒவ்வொரு கோரிக்கையிலும் வேறுபட்ட இணைப்புக்கு மாறுகின்றன. பல-முறை அரட்டைகளுக்கான prompt-cache ஹிட்களைப் பாதுகாக்க இதை முடக்கியே வைக்கவும். Per-combo மேலெழுதல்களுக்கு முன்னுரிமை உண்டு.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "கிரெடென்ஷியல் மறைத்தல்", "credentialRedactionDesc": "வழங்குநர்களுக்கு அனுப்பப்படும் சூழல் மற்றும் பதில்களில் இருந்து API விசைகள், டோக்கன்கள் மற்றும் ரகசியங்களை மறைக்கவும்.", "enableCredentialRedaction": "கிரெடென்ஷியல் மறைத்தலை இயக்கு", @@ -8621,6 +8623,27 @@ }, "enableTitle": "இயந்திரத்தை இயக்கு", "enableDescription": "அடுக்கில் கடைசியாக இயங்குகிறது (RTK/Caveman உரையைச் சுத்தப்படுத்திய பிறகு, OmniGlyph எஞ்சியிருப்பதை படங்களாக மாற்றுகிறது) மேலும் omniglyph பயன்முறை மூலமாகவும் தனித்து இயங்குகிறது. இது ஒரு முன்னோட்டமாகும், மேலும் இறுதி-முதல்-இறுதி சரிபார்ப்பு முடியும் வரை இயல்பாகவே முடக்கப்பட்டிருக்கும்.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "சேமிக்கப்பட்டது.", "saveFailed": "சேமிக்க முடியவில்லை.", "enableAria": "OmniGlyph இயந்திரத்தை இயக்கு", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "அதிகतम", "grokAutoTopUpMonth": "மாதம்", "grokAdditionalCredits": "கூடுதல் நிதிகள்", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "முதல் டோக்கன்", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 58688a4858..73032e0d13 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "దృశ్య అభ్యర్థన కాలరేఖ", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "తిరిగి తెరువు", "close": "మూసివేయండి" }, - "noResults": "ఫలితాలు లేవు", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "ఫలితాలు లేవు" }, "webhooks": { "title": "వెబ్‌బూక్స్", @@ -1739,8 +1739,8 @@ "quotaShare": "కోటా షేర్", "discovery": "అన్వేషణ", "freeProviderRankings": "ఉచిత ప్రొవైడర్ ర్యాంకింగ్‌లు", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "ఉచిత శ్రేణులు", "gamification": "గేమిఫికేషన్", "leaderboard": "లీడర్‌బోర్డ్", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "ఈ ప్రొవైడర్ నిలిపివేయబడింది", "riskNotice": { "title": "కొనసాగడానికి ముందు", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "వినియోగ హెచ్చరికలు ఉన్న ప్రొవైడర్ — వివరాల కోసం క్లిక్ చేయండి", "oauth": "ఈ ప్రొవైడర్ మీ అధికారిక ప్రోడక్ట్ సెషన్/OAuthని ఉపయోగిస్తుంది, ఇది ప్రాక్సీ/రూటర్ వినియోగానికి అనుమతించబడలేదు. మేము తీవ్రమైన అటానమస్ ఏజెంట్ వినియోగాన్ని (OpenCloud-శైలి, సుదీర్ఘ బహుళ-దశల ఫ్లోలు, పెద్ద బ్యాచ్‌లు) సిఫార్సు చేయము — అప్‌స్ట్రీమ్ ఖాతాను పరిమితం చేయడం లేదా నిషేధించడం ద్వారా ప్రతిస్పందించవచ్చు. మీ స్వంత పూచీకత్తుపై ఉపయోగించండి.", "webCookie": "ఈ ప్రొవైడర్ మీ వెబ్ సెషన్ కుకీల ద్వారా ప్రామాణీకరిస్తుంది. అప్‌స్ట్రీమ్ సేవ ఎప్పుడైనా సెషన్‌ను చెల్లనిదిగా చేయవచ్చు, దీని వలన మీరు మళ్లీ లాగిన్ అవ్వాల్సి ఉంటుంది. ఎక్కువసేపు పర్యవేక్షణ లేని ఆపరేషన్ల కోసం సిఫార్సు చేయబడదు. మీ స్వంత పూచీకత్తుపై ఉపయోగించండి.", @@ -5107,9 +5111,9 @@ "cancel": "రద్దు చేయండి" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "ఆటో-ఫెచ్ అప్‌స్ట్రీమ్ మోడల్స్", + "autoFetchModelsTooltip": "అవసరమైనప్పుడు అప్‌స్ట్రీమ్ మోడల్స్‌ను పొందండి మరియు కాష్ చేయండి", + "autoFetchModelsEnabled": "అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్ ప్రారంభించబడింది", + "autoFetchModelsDisabled": "అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్ నిలిపివేయబడింది", + "autoFetchModelsToggleFailed": "అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్‌ను టోగుల్ చేయడంలో విఫలమైంది", + "autoFetchModelsPartialFailure": "కొన్ని కనెక్షన్లు నవీకరించబడ్డాయి, కానీ అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్ ప్రతి చోట మారలేదు", + "overridesUpstreamModel": "అప్‌స్ట్రీమ్‌ను ఓవర్‌రైడ్ చేయండి", + "overridesUpstreamModelHint": "మీ సెట్టింగ్స్ ఈ అప్‌స్ట్రీమ్ మోడల్‌ను అధిగమిస్తాయి", + "resetToUpstreamDefaults": "అప్‌స్ట్రీమ్ డిఫాల్ట్స్‌ను పునరుద్ధరించండి", + "resetToUpstreamDefaultsSuccess": "అప్‌స్ట్రీమ్ మోడల్ డిఫాల్ట్స్ పునరుద్ధరించబడ్డాయి", + "resetToUpstreamDefaultsFailed": "అప్‌స్ట్రీమ్ మోడల్ డిఫాల్ట్స్‌ను పునరుద్ధరించడంలో విఫలమైంది", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "స్థానిక web_fetch టూల్ కాల్‌లను OmniRoute యొక్క /v1/web/fetch కి రీరైట్ చేయండి.", "interceptionLoadError": "ఇంటర్‌సెప్షన్ సెట్టింగ్‌లను లోడ్ చేయడం విఫలమైంది: {error}", "interceptionSaveError": "ఇంటర్‌సెప్షన్ సెట్టింగ్‌లను సేవ్ చేయడం విఫలమైంది: {error}", - "ccAliasSectionTitle": "Claude కోడ్‌లో ఎక్స్‌పోజ్ చేయండి (claude/…)", - "ccAliasSectionHint": "ఈ ప్రొవైడర్ యొక్క మోడళ్లను claude/<provider>/<model> మిర్రర్ ఐడీల క్రింద ప్రచారం చేయండి, కాబట్టి Claude Code యొక్క గేట్వే మోడల్ డిస్కవరీ వాటిని జాబితా చేయగలదు. డిఫాల్ట్‌గా ఆఫ్ - దీన్ని ప్రారంభించడం అన్ని క్లయింట్ల కోసం కాటలాగ్ ఎంట్రీలను రెండింతలు చేస్తుంది.", - "ccAliasProviderLevelLabel": "ప్రదాత డిఫాల్ట్", - "ccAliasModelOverridesLabel": "ప్రతి మోడల్ కోసం ఓవర్‌రైడ్స్", - "ccAliasModelOverrideAriaLabel": "{modelId} కోసం ఓవర్‌రైడ్", - "ccAliasStateInherit": "వారసత్వం", - "ccAliasStateOn": "పై", - "ccAliasStateOff": "ఆఫ్", - "ccAliasAddModelPlaceholder": "మోడల్ ఐడి (ఉదాహరణకు gpt-4o)", - "ccAliasAddModelButton": "ఓవర్‌రైడ్ జోడించండి", - "ccAliasLoadError": "డిస్కవరీ-అలియాస్ సెట్టింగ్స్ లోడ్ చేయడంలో విఫలమైంది: {error}", - "ccAliasSaveError": "discovery-alias సెట్టింగ్‌ను సేవ్ చేయడంలో విఫలమైంది: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "API కీతో Galadrielని కనెక్ట్ చేయండి.", "predibase": "$25 ఉచిత ట్రయల్ క్రెడిట్‌లు (30 రోజుల చెల్లుబాటు)", "chenzk": "chenzk.top వద్ద లైవ్ మోడల్ కేటలాగ్‌తో కూడిన OpenAI-అనుకూల గేట్‌వే.", - "freepik": "Freepik యొక్క Mystic APIతో చిత్రాలను రూపొందించండి.", + "magnific": "Freepik యొక్క Mystic APIతో చిత్రాలను రూపొందించండి.", "freetheai": "పాస్‌త్రూ మోడల్ మద్దతుతో కూడిన ఉచిత OpenAI-అనుకూల గేట్‌వే.", "g4f-gemini": "Geminiకి ఉచిత నో-కీ g4f.space రివర్స్ ప్రాక్సీ, నిమిషానికి 5 అభ్యర్థనలకు పరిమితం చేయబడింది.", "g4f-groq": "Groqకి ఉచిత నో-కీ g4f.space రివర్స్ ప్రాక్సీ, నిమిషానికి 5 అభ్యర్థనలకు పరిమితం చేయబడింది.", @@ -6209,6 +6224,7 @@ "claude": "ఇప్పటికే ఉన్న OAuth ఫ్లోతో Claude Codeని కనెక్ట్ చేయండి.", "cline": "ఇప్పటికే ఉన్న OAuth ఫ్లోతో Clineని కనెక్ట్ చేయండి.", "cursor": "ఇప్పటికే ఉన్న OAuth ఫ్లోతో Cursor IDEని కనెక్ట్ చేయండి.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "ఇప్పటికే ఉన్న OAuth ఫ్లోతో GitHub Copilotని కనెక్ట్ చేయండి.", "gitlab-duo": "ai_features + read_user స్కోప్‌లతో కూడిన OAuth అప్లికేషన్. ఈ OmniRoute ఇన్‌స్టాన్స్‌లో GITLAB_DUO_OAUTH_CLIENT_ID మరియు ఐచ్ఛికంగా GITLAB_DUO_OAUTH_CLIENT_SECRETని కాన్ఫిగర్ చేయండి.", "kilocode": "ఇప్పటికే ఉన్న OAuth ఫ్లోతో Kilo Codeని కనెక్ట్ చేయండి.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "నిరీక్షణ వ్యవధిలో ఉంది", "codexPoolUsed": "ఉపయోగించబడింది", "codexPoolUntil": "{value} వరకు", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "అనామక ఫాల్బ్యాక్", "anonymousFallbackDesc": "అన్ని కాన్ఫిగర్ చేసిన కనెక్షన్లు ముగిసినప్పుడు (కోటా, క్రెడిట్స్, లేదా కాలం ముగిసినప్పుడు), తాత్కాలికంగా ఈ ప్రొవైడర్ యొక్క కీ లెస్ టియర్‌ను ఉపయోగించండి. అనామక అభ్యర్థనలను పంపించకుండా ఈ ప్రొవైడర్‌ను దాటించడానికి ఆపివేయండి — కీ లెస్ టియర్ వాటిని తిరస్కరించినప్పుడు (401) సిఫారసు చేయబడింది.", "anonymousFallbackEnabled": "{provider} కోసం అనామక ఫాల్బ్యాక్ ప్రారంభించబడింది", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "సేవ్ చేసిన మోడల్ ఎండ్‌పాయింట్ సెట్టింగ్స్", "searchByModelAria": "మోడల్ ద్వారా శోధించండి", "selectSupportedEndpoint": "కమిషన్ చేయబడిన కనెక్ట్ చేయబడిన ఎండ్‌పాయింట్‌లలో కనీసం ఒకటి ఎంచుకోండి", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsTooltip": "అవసరమైనప్పుడు అప్‌స్ట్రీమ్ మోడల్స్‌ను పొందండి మరియు కాష్ చేయండి", - "autoFetchModelsEnabled": "అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్ ప్రారంభించబడింది", - "autoFetchModels": "ఆటో-ఫెచ్ అప్‌స్ట్రీమ్ మోడల్స్", - "autoFetchModelsDisabled": "అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్ నిలిపివేయబడింది", - "overridesUpstreamModelHint": "మీ సెట్టింగ్స్ ఈ అప్‌స్ట్రీమ్ మోడల్‌ను అధిగమిస్తాయి", - "overridesUpstreamModel": "అప్‌స్ట్రీమ్‌ను ఓవర్‌రైడ్ చేయండి", - "autoFetchModelsToggleFailed": "అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్‌ను టోగుల్ చేయడంలో విఫలమైంది", - "autoFetchModelsPartialFailure": "కొన్ని కనెక్షన్లు నవీకరించబడ్డాయి, కానీ అప్‌స్ట్రీమ్ మోడల్ ఆటో-ఫెచ్ ప్రతి చోట మారలేదు", - "resetToUpstreamDefaultsSuccess": "అప్‌స్ట్రీమ్ మోడల్ డిఫాల్ట్స్ పునరుద్ధరించబడ్డాయి", - "resetToUpstreamDefaults": "అప్‌స్ట్రీమ్ డిఫాల్ట్స్‌ను పునరుద్ధరించండి", - "resetToUpstreamDefaultsFailed": "అప్‌స్ట్రీమ్ మోడల్ డిఫాల్ట్స్‌ను పునరుద్ధరించడంలో విఫలమైంది" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "నిషేధించబడిన కీలకపదాలు", "customBannedSignalsDesc": "శాశ్వత ఖాతా నిషేధ గుర్తింపును ప్రేరేపించే అదనపు కీలకపదాలు. అంతర్నిర్మిత కీలకపదాలు ఎల్లప్పుడూ వర్తిస్తాయి.", "customBannedSignalsPlaceholder": "ఉదా. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "కాన్ఫిగర్ చేయబడింది", "none": "ఏదీ లేదు", "modelOverrideValuePlaceholder": "సంఖ్యా విలువ", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "కీ విలువను జోడించండి", "noModelOverrides": "ఈ మోడల్ కోసం ఎటువంటి ఓవర్‌రైడ్‌లు కాన్ఫిగర్ చేయబడలేదు.", "modelOverrideLoadFailed": "మోడల్ ఓవర్‌రైడ్‌లను లోడ్ చేయడం విఫలమైంది", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "సంక్షిప్త CJK (文言)", "description": "క్లాసికల్-చైనీస్ అల్ట్రా-సంక్షిప్త శైలి (చైనీస్ కోసం మాత్రమే అందుబాటులో ఉంది)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "రౌండ్-రాబిన్ మరియు యాదృచ్ఛిక కాంబోలు మొదటి-సందేశం హ్యాష్ ద్వారా మొత్తం సంభాషణను ఒకే కనెక్షన్‌కు పిన్ చేయడానికి బదులుగా ప్రతి అభ్యర్థనపై వేరే కనెక్షన్‌కు మారుతాయి. మల్టీ-టర్న్ చాట్‌ల కోసం ప్రాంప్ట్-క్యాచీ హిట్‌లను అలాగే ఉంచడానికి దీనిని నిలిపివేయండి. ప్రతి కాంబో ఓవర్‌రైడ్‌లు ప్రాధాన్యతను కలిగి ఉంటాయి.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "క్రెడెన్షియల్ రెడాక్షన్", "credentialRedactionDesc": "ప్రొవైడర్‌లకు పంపిన సందర్భం నుండి మరియు ప్రతిస్పందనల నుండి API కీలు, టోకెన్‌లు మరియు రహస్యాలను తొలగించండి.", "enableCredentialRedaction": "క్రెడెన్షియల్ రెడాక్షన్‌ను ప్రారంభించండి", @@ -8621,6 +8623,27 @@ }, "enableTitle": "ఇంజిన్‌ను ఎనేబుల్ చేయండి", "enableDescription": "స్టాక్‌లో చివరిగా రన్ అవుతుంది (RTK/Caveman వచనాన్ని క్లీన్ చేసిన తర్వాత, OmniGlyph మిగిలిన భాగాన్ని చిత్రాలుగా మారుస్తుంది) మరియు omniglyph మోడ్ ద్వారా స్వతంత్రంగా కూడా రన్ అవుతుంది. ఇది ప్రివ్యూ మరియు ఎండ్-టు-ఎండ్ ధ్రువీకరణ పూర్తయ్యే వరకు డిఫాల్ట్‌గా ఆఫ్‌లోనే ఉంటుంది.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "సేవ్ చేయబడింది.", "saveFailed": "సేవ్ చేయడం సాధ్యపడలేదు.", "enableAria": "OmniGlyph ఇంజిన్‌ను ఎనేబుల్ చేయండి", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "గరిష్టం", "grokAutoTopUpMonth": "మాసం", "grokAdditionalCredits": "అదనపు క్రెడిట్స్", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "మొదటి టోకెన్", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index ed1e5d4063..4b004659a9 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "ไทม์ไลน์คำขอภาพ", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "เปิด", "close": "ปิด" }, - "noResults": "ไม่มีผลลัพธ์", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "ไม่มีผลลัพธ์" }, "webhooks": { "title": "เว็บฮุค", @@ -1739,8 +1739,8 @@ "quotaShare": "การแชร์โควตา", "discovery": "การค้นพบ", "freeProviderRankings": "อันดับผู้ให้บริการฟรี", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "แพ็กเกจฟรี", "gamification": "เกมมิฟิเคชัน", "leaderboard": "กระดานผู้นำ", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "ผู้ให้บริการรายนี้เลิกใช้แล้ว", "riskNotice": { "title": "ก่อนดำเนินการต่อ", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "ผู้ให้บริการที่มีข้อควรระวังในการใช้งาน — คลิกเพื่อดูรายละเอียด", "oauth": "ผู้ให้บริการรายนี้ใช้เซสชันผลิตภัณฑ์อย่างเป็นทางการ/OAuth ของคุณ ซึ่งไม่ได้รับอนุญาตให้ใช้กับพร็อกซี/เราเตอร์ เราไม่แนะนำให้ใช้งานเอเจนต์อัตโนมัติอย่างหนักหน่วง (เช่น สไตล์ OpenCloud, โฟลว์หลายขั้นตอนที่ยาวนาน, การประมวลผลแบบกลุ่มขนาดใหญ่) — ต้นทางอาจตอบสนองโดยการจำกัดหรือแบนบัญชี ใช้งานโดยยอมรับความเสี่ยงด้วยตนเอง", "webCookie": "ผู้ให้บริการรายนี้ยืนยันตัวตนผ่านคุกกี้เซสชันเว็บของคุณ บริการต้นทางอาจทำให้เซสชันหมดอายุเมื่อใดก็ได้ ซึ่งจะทำให้คุณต้องเข้าสู่ระบบใหม่อีกครั้ง ไม่แนะนำสำหรับการทำงานระยะยาวที่ไม่มีการเฝ้าดูแล ใช้งานโดยยอมรับความเสี่ยงด้วยตนเอง", @@ -5107,9 +5111,9 @@ "cancel": "ยกเลิก" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "ปิดการใช้งาน", "enableProvider": "เปิดใช้งานผู้ให้บริการ", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "ข้าม {count} โมเดลที่มีอยู่", "autoSync": "ซิงค์อัตโนมัติ", "autoSyncShort": "ซิงค์", + "autoFetchModels": "ดึงโมเดลจาก upstream อัตโนมัติ", + "autoFetchModelsTooltip": "ดึงและเก็บโมเดลจากต้นทางเมื่อจำเป็น", + "autoFetchModelsEnabled": "เปิดใช้งานการดึงข้อมูลโมเดลจากต้นทางอัตโนมัติ", + "autoFetchModelsDisabled": "การดึงข้อมูลโมเดลจากต้นทางถูกปิดใช้งาน", + "autoFetchModelsToggleFailed": "ไม่สามารถเปลี่ยนการดึงข้อมูลอัตโนมัติของโมเดล upstream ได้", + "autoFetchModelsPartialFailure": "การเชื่อมต่อบางรายการได้รับการอัปเดต แต่การดึงข้อมูลโมเดลต้นน้ำอัตโนมัติไม่ได้เปลี่ยนแปลงในทุกที่", + "overridesUpstreamModel": "เขียนทับต้นทาง", + "overridesUpstreamModelHint": "การตั้งค่าของคุณจะมีผลเหนือโมเดลต้นทางนี้", + "resetToUpstreamDefaults": "คืนค่าการตั้งค่าเริ่มต้นของ upstream", + "resetToUpstreamDefaultsSuccess": "กู้คืนค่าเริ่มต้นของโมเดลต้นทาง", + "resetToUpstreamDefaultsFailed": "ไม่สามารถกู้คืนค่าเริ่มต้นของโมเดลต้นน้ำได้", "autoSyncTooltip": "รีเฟรชรายการโมเดลโดยอัตโนมัติทุกๆ 24 ชั่วโมง (กำหนดค่าได้ผ่าน MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "เปิดใช้งานการซิงค์อัตโนมัติ — โมเดลจะรีเฟรชเป็นระยะ", "autoSyncDisabled": "ปิดใช้งานการซิงค์อัตโนมัติแล้ว", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "เขียนทับการเรียกใช้เครื่องมือ web_fetch ดั้งเดิมไปยัง /v1/web/fetch ของ OmniRoute", "interceptionLoadError": "โหลดการตั้งค่าการสกัดกั้นล้มเหลว: {error}", "interceptionSaveError": "บันทึกการตั้งค่าการสกัดกั้นล้มเหลว: {error}", - "ccAliasSectionTitle": "เปิดเผยใน Claude Code (claude/…)", - "ccAliasSectionHint": "โฆษณาโมเดลของผู้ให้บริการนี้ภายใต้ claude/<provider>/<model> mirror ids เพื่อให้การค้นหาโมเดลของ Claude Code สามารถแสดงรายการได้ ปิดโดยค่าเริ่มต้น — การเปิดใช้งานนี้จะทำให้รายการในแคตตาล็อกเพิ่มเป็นสองเท่าสำหรับลูกค้าทุกคน.", - "ccAliasProviderLevelLabel": "ผู้ให้บริการเริ่มต้น", - "ccAliasModelOverridesLabel": "การเขียนทับต่อโมเดลแต่ละตัว", - "ccAliasModelOverrideAriaLabel": "การแทนที่สำหรับ {modelId}", - "ccAliasStateInherit": "สืบทอด", - "ccAliasStateOn": "เปิด", - "ccAliasStateOff": "ปิด", - "ccAliasAddModelPlaceholder": "รหัสโมเดล (เช่น gpt-4o)", - "ccAliasAddModelButton": "เพิ่มการเขียนทับ", - "ccAliasLoadError": "ไม่สามารถโหลดการตั้งค่า discovery-alias ได้: {error}", - "ccAliasSaveError": "ไม่สามารถบันทึกการตั้งค่า discovery-alias ได้: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "เชื่อมต่อ Galadriel ด้วยคีย์ API", "predibase": "เครดิตทดลองใช้ฟรี $25 (มีอายุ 30 วัน)", "chenzk": "เกตเวย์ที่เข้ากันได้กับ OpenAI พร้อมแคตตาล็อกโมเดลแบบสดที่ chenzk.top", - "freepik": "สร้างรูปภาพด้วย Mystic API ของ Freepik", + "magnific": "สร้างรูปภาพด้วย Mystic API ของ Freepik", "freetheai": "เกตเวย์ฟรีที่เข้ากันได้กับ OpenAI พร้อมการรองรับโมเดลแบบ passthrough", "g4f-gemini": "รีเวิร์สพร็อกซี g4f.space ฟรีแบบไม่ต้องใช้คีย์ไปยัง Gemini จำกัด 5 คำขอต่อนาที", "g4f-groq": "รีเวิร์สพร็อกซี g4f.space ฟรีแบบไม่ต้องใช้คีย์ไปยัง Groq จำกัด 5 คำขอต่อนาที", @@ -6209,6 +6224,7 @@ "claude": "เชื่อมต่อ Claude Code ด้วยโฟลว์ OAuth ที่มีอยู่", "cline": "เชื่อมต่อ Cline ด้วยโฟลว์ OAuth ที่มีอยู่", "cursor": "เชื่อมต่อ Cursor IDE ด้วยโฟลว์ OAuth ที่มีอยู่", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "เชื่อมต่อ GitHub Copilot ด้วยโฟลว์ OAuth ที่มีอยู่", "gitlab-duo": "แอปพลิเคชัน OAuth ที่มีขอบเขต ai_features + read_user กำหนดค่า GITLAB_DUO_OAUTH_CLIENT_ID และ GITLAB_DUO_OAUTH_CLIENT_SECRET (ไม่บังคับ) บนอินสแตนซ์ OmniRoute นี้", "kilocode": "เชื่อมต่อ Kilo Code ด้วยโฟลว์ OAuth ที่มีอยู่", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "อยู่ในช่วงพัก", "codexPoolUsed": "ใช้แล้ว", "codexPoolUntil": "จนถึง {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "การสำรองข้อมูลแบบไม่ระบุชื่อ", "anonymousFallbackDesc": "เมื่อการเชื่อมต่อที่กำหนดทั้งหมดหมดลง (โควตา, เครดิต, หรือหมดอายุ) ให้ใช้ชั้นที่ไม่มีคีย์ของผู้ให้บริการนี้ชั่วคราว ปิดเพื่อข้ามผู้ให้บริการนี้แทนที่จะส่งคำขอแบบไม่ระบุชื่อ — แนะนำเมื่อชั้นที่ไม่มีคีย์ปฏิเสธคำขอเหล่านั้น (401).", "anonymousFallbackEnabled": "เปิดใช้งานการสำรองข้อมูลแบบไม่ระบุชื่อสำหรับ {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "การตั้งค่า endpoint ของโมเดลที่บันทึกไว้", "searchByModelAria": "ค้นหาตามรุ่น", "selectSupportedEndpoint": "เลือกจุดสิ้นสุดที่รองรับอย่างน้อยหนึ่งจุด", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsTooltip": "ดึงและเก็บโมเดลจากต้นทางเมื่อจำเป็น", - "autoFetchModels": "ดึงโมเดลจาก upstream อัตโนมัติ", - "autoFetchModelsEnabled": "เปิดใช้งานการดึงข้อมูลโมเดลจากต้นทางอัตโนมัติ", - "autoFetchModelsDisabled": "การดึงข้อมูลโมเดลจากต้นทางถูกปิดใช้งาน", - "overridesUpstreamModel": "เขียนทับต้นทาง", - "overridesUpstreamModelHint": "การตั้งค่าของคุณจะมีผลเหนือโมเดลต้นทางนี้", - "autoFetchModelsToggleFailed": "ไม่สามารถเปลี่ยนการดึงข้อมูลอัตโนมัติของโมเดล upstream ได้", - "autoFetchModelsPartialFailure": "การเชื่อมต่อบางรายการได้รับการอัปเดต แต่การดึงข้อมูลโมเดลต้นน้ำอัตโนมัติไม่ได้เปลี่ยนแปลงในทุกที่", - "resetToUpstreamDefaults": "คืนค่าการตั้งค่าเริ่มต้นของ upstream", - "resetToUpstreamDefaultsSuccess": "กู้คืนค่าเริ่มต้นของโมเดลต้นทาง", - "resetToUpstreamDefaultsFailed": "ไม่สามารถกู้คืนค่าเริ่มต้นของโมเดลต้นน้ำได้" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "การตั้งค่า", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "คีย์เวิร์ดที่ถูกแบน", "customBannedSignalsDesc": "คีย์เวิร์ดเพิ่มเติมที่ทริกเกอร์การตรวจจับการแบนบัญชีถาวร คีย์เวิร์ดในตัวจะมีผลเสมอ", "customBannedSignalsPlaceholder": "เช่น api key revoked", @@ -7210,6 +7203,7 @@ "configured": "กำหนดค่าแล้ว", "none": "ไม่มี", "modelOverrideValuePlaceholder": "ค่าตัวเลข", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "เพิ่มคีย์-ค่า", "noModelOverrides": "ไม่มีการกำหนดค่าการเขียนทับสำหรับโมเดลนี้", "modelOverrideLoadFailed": "โหลดการเขียนทับโมเดลล้มเหลว", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "CJK แบบกระชับ (文言)", "description": "สไตล์ภาษาจีนคลาสสิกแบบกระชับอย่างยิ่ง (ใช้ได้เฉพาะภาษาจีนเท่านั้น)" @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "คอมโบแบบ Round-robin และแบบสุ่มจะสลับไปยังการเชื่อมต่ออื่นในทุกคำขอ แทนที่จะตรึงการสนทนาทั้งหมดไว้กับการเชื่อมต่อเดียวตามแฮชของข้อความแรก ปิดไว้เพื่อรักษา prompt-cache hits สำหรับการแชทแบบหลายรอบ การแทนที่ระดับคอมโบจะมีผลเหนือกว่า", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "การปกปิดข้อมูลรับรอง", "credentialRedactionDesc": "ปกปิดคีย์ API, โทเค็น และข้อมูลลับจากบริบทที่ส่งไปยังผู้ให้บริการและจากการตอบกลับ", "enableCredentialRedaction": "เปิดใช้งานการปกปิดข้อมูลรับรอง", @@ -8621,6 +8623,27 @@ }, "enableTitle": "เปิดใช้งานเอนจิน", "enableDescription": "ทำงานเป็นลำดับสุดท้ายในสแตก (หลังจาก RTK/Caveman คลีนข้อความ และ OmniGlyph แปลงส่วนที่เหลือเป็นรูปภาพ) และยังทำงานแบบสแตนด์อโลนผ่านโหมด omniglyph นี่เป็นเวอร์ชันพรีวิวและจะยังคงปิดใช้งานไว้เป็นค่าเริ่มต้นจนกว่าการตรวจสอบความถูกต้องแบบครบวงจร (end-to-end) จะเสร็จสิ้น", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "บันทึกแล้ว", "saveFailed": "ไม่สามารถบันทึกได้", "enableAria": "เปิดใช้งานเอนจิน OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "สูงสุด", "grokAutoTopUpMonth": "เดือน", "grokAdditionalCredits": "เครดิตเพิ่มเติม", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "คนตัดไม้", "proxyTab": "หนังสือมอบฉันทะ", "budgetManagement": "การจัดการงบประมาณ", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "โทเค็นแรก", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 5ef5816345..abd8bfc297 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Görsel istek zaman çizelgesi", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "açık", "close": "kapat" }, - "noResults": "Sonuç yok", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Sonuç yok" }, "webhooks": { "title": "Web kancaları", @@ -1739,8 +1739,8 @@ "quotaShare": "Kota Payı", "discovery": "Keşif", "freeProviderRankings": "Ücretsiz Sağlayıcı Sıralamaları", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Ücretsiz Katmanlar", "gamification": "Oyunlaştırma", "leaderboard": "Liderlik Tablosu", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Bu sağlayıcı kullanımdan kaldırıldı", "riskNotice": { "title": "Devam etmeden önce", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Kullanım uyarıları olan sağlayıcı — ayrıntılar için tıklayın", "oauth": "Bu sağlayıcı, proxy/yönlendirici kullanımı için yetkilendirilmemiş resmi ürün oturumunuzu/OAuth'unuzu kullanır. Yoğun otonom ajan kullanımını (OpenCloud tarzı, uzun çok adımlı akışlar, büyük toplu işlemler) önermiyoruz — üst sağlayıcı hesabı kısıtlayarak veya yasaklayarak tepki verebilir. Kullanım riski size aittir.", "webCookie": "Bu sağlayıcı, web oturumu çerezleriniz aracılığıyla kimlik doğrulaması yapar. Üst servis oturumu istediği zaman geçersiz kılabilir ve tekrar giriş yapmanızı gerektirebilir. Uzun süreli gözetimsiz işlemler için önerilmez. Kullanım riski size aittir.", @@ -5107,9 +5111,9 @@ "cancel": "İptal" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Devre dışı", "enableProvider": "Sağlayıcıyı etkinleştir", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "{count} mevcut model atlanıyor", "autoSync": "Otomatik Senkronizasyon", "autoSyncShort": "Senkronize Et", + "autoFetchModels": "Otomatik olarak üst akış modellerini al", + "autoFetchModelsTooltip": "Gerekli olduğunda yukarı akış modellerini al ve önbelleğe al", + "autoFetchModelsEnabled": "Üst akış modeli otomatik alma etkinleştirildi", + "autoFetchModelsDisabled": "Üst akış modeli otomatik alma devre dışı bırakıldı", + "autoFetchModelsToggleFailed": "Yukarı akış model otomatik alımını değiştirme başarısız oldu", + "autoFetchModelsPartialFailure": "Bazı bağlantılar güncellendi, ancak yukarı akış modeli otomatik alımı her yerde değişmedi.", + "overridesUpstreamModel": "Üst akışı geçersiz kılar", + "overridesUpstreamModelHint": "Ayarlarınız bu üst modelin üzerine yazıyor.", + "resetToUpstreamDefaults": "Varsayılan ayarları geri yükle", + "resetToUpstreamDefaultsSuccess": "Yukarı akış model varsayılanları geri yüklendi", + "resetToUpstreamDefaultsFailed": "Üst akış model varsayılanlarını geri yükleme başarısız oldu", "autoSyncTooltip": "Model listesini her 24 saatte bir otomatik olarak yenileyin (MODEL_SYNC_INTERVAL_HOURS aracılığıyla yapılandırılabilir)", "autoSyncEnabled": "Otomatik senkronizasyon etkin — modeller periyodik olarak yenilenecek", "autoSyncDisabled": "Otomatik senkronizasyon devre dışı bırakıldı", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Yerel web_fetch araç çağrılarını OmniRoute'un /v1/web/fetch uç noktasına yeniden yazar.", "interceptionLoadError": "Yakalama ayarları yüklenemedi: {error}", "interceptionSaveError": "Yakalama ayarları kaydedilemedi: {error}", - "ccAliasSectionTitle": "Claude Kodu'nda Açığa Çıkar (claude/…)", - "ccAliasSectionHint": "Bu sağlayıcının modellerini claude/<provider>/<model> ayna kimlikleri altında tanıtın, böylece Claude Code'un geçiş modeli keşfi bunları listeleyebilir. Varsayılan olarak kapalı — bunu etkinleştirmek, tüm müşteriler için katalog girişlerini iki katına çıkarır.", - "ccAliasProviderLevelLabel": "Sağlayıcı varsayılan", - "ccAliasModelOverridesLabel": "Model başına geçersiz kılmalar", - "ccAliasModelOverrideAriaLabel": "{modelId} için geçersiz kılma", - "ccAliasStateInherit": "Devralmak", - "ccAliasStateOn": "Açık", - "ccAliasStateOff": "Kapalı", - "ccAliasAddModelPlaceholder": "Model kimliği (ör. gpt-4o)", - "ccAliasAddModelButton": "Override ekle", - "ccAliasLoadError": "Keşif-alias ayarlarını yüklemek başarısız oldu: {error}", - "ccAliasSaveError": "discovery-alias ayarını kaydetme başarısız oldu: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Ek yukarı akış başlıkları", "compatUpstreamHeadersHint": "Yüksek ayrıcalıklı ayar: sağlayıcı API kimlik bilgilerini düzenlemekle aynı güven düzeyine sahiptir; yalnızca güvenilir yöneticiler kullanmalıdır. OmniRoute, sağlayıcı API anahtarından kimlik doğrulama başlığını ekledikten sonra bu başlıklar birleştirilir. Özel bir başlık mevcut bir başlıkla aynı adı kullanıyorsa (ör. Authorization), verdiğiniz değer otomatik oluşturulan başlığın (Bearer token dahil) tamamının yerini alır; yukarı akış yalnızca sizin yazdığınız değeri görür. Hatalı yapılandırma 401 hatalarına veya bozuk yukarı akış kimlik doğrulamasına yol açabilir. Her başlık için bir satır kullanın (ör. bazı geçitler için ek Authentication başlığı). Önizlemek için değerin üzerine gelin veya odaklayın. Bu panelde bulanıklaştırma, dışarı tıklama veya paneli kapatma sırasında otomatik kaydedilir.", "compatUpstreamHeaderName": "Başlık adı", @@ -6194,7 +6209,7 @@ "galadriel": "Galadriel'i bir API anahtarı ile bağlayın.", "predibase": "25$ ücretsiz deneme kredisi (30 gün geçerli)", "chenzk": "chenzk.top adresinde canlı model kataloğuna sahip OpenAI uyumlu ağ geçidi.", - "freepik": "Freepik'in Mystic API'si ile görseller oluşturun.", + "magnific": "Freepik'in Mystic API'si ile görseller oluşturun.", "freetheai": "Doğrudan geçişli (passthrough) model destekli, ücretsiz OpenAI uyumlu ağ geçidi.", "g4f-gemini": "Gemini için anahtarsız, ücretsiz g4f.space ters proxy'si, dakikada 5 istek ile sınırlıdır.", "g4f-groq": "Groq için anahtarsız, ücretsiz g4f.space ters proxy'si, dakikada 5 istek ile sınırlıdır.", @@ -6209,6 +6224,7 @@ "claude": "Mevcut OAuth akışıyla Claude Code'u bağlayın.", "cline": "Mevcut OAuth akışıyla Cline'ı bağlayın.", "cursor": "Mevcut OAuth akışıyla Cursor IDE'yi bağlayın.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Mevcut OAuth akışıyla GitHub Copilot'ı bağlayın.", "gitlab-duo": "ai_features + read_user kapsamlarına sahip OAuth uygulaması. Bu OmniRoute örneğinde GITLAB_DUO_OAUTH_CLIENT_ID ve isteğe bağlı olarak GITLAB_DUO_OAUTH_CLIENT_SECRET yapılandırın.", "kilocode": "Mevcut OAuth akışıyla Kilo Code'u bağlayın.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "Bekleme süresinde", "codexPoolUsed": "kullanıldı", "codexPoolUntil": "{value} tarihine kadar", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Anonim yedekleme", "anonymousFallbackDesc": "Tüm yapılandırılmış bağlantılar tükendiğinde (kota, kredi veya süresi dolmuş), bu sağlayıcının anahtarsız katmanını geçici olarak kullanın. Anahtarsız katmanın bunları reddettiği (401) durumlarda, anonim istek göndermek yerine bu sağlayıcıyı atlamak için kapatın — önerilir.", "anonymousFallbackEnabled": "{provider} için anonim geri dönüş etkinleştirildi", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Kaydedilmiş model uç noktası ayarları", "searchByModelAria": "Model ile ara", "selectSupportedEndpoint": "En az bir desteklenen uç noktayı seçin", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "Üst akış modeli otomatik alma devre dışı bırakıldı", - "autoFetchModels": "Otomatik olarak üst akış modellerini al", - "autoFetchModelsTooltip": "Gerekli olduğunda yukarı akış modellerini al ve önbelleğe al", - "autoFetchModelsEnabled": "Üst akış modeli otomatik alma etkinleştirildi", - "autoFetchModelsToggleFailed": "Yukarı akış model otomatik alımını değiştirme başarısız oldu", - "autoFetchModelsPartialFailure": "Bazı bağlantılar güncellendi, ancak yukarı akış modeli otomatik alımı her yerde değişmedi.", - "overridesUpstreamModel": "Üst akışı geçersiz kılar", - "overridesUpstreamModelHint": "Ayarlarınız bu üst modelin üzerine yazıyor.", - "resetToUpstreamDefaults": "Varsayılan ayarları geri yükle", - "resetToUpstreamDefaultsSuccess": "Yukarı akış model varsayılanları geri yüklendi", - "resetToUpstreamDefaultsFailed": "Üst akış model varsayılanlarını geri yükleme başarısız oldu" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Ayarlar", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Yasaklı Anahtar Kelimeler", "customBannedSignalsDesc": "Kalıcı hesap engelleme algılamasını tetikleyen ek anahtar kelimeler. Yerleşik anahtar kelimeler her zaman geçerlidir.", "customBannedSignalsPlaceholder": "örn. api key revoked", @@ -7210,6 +7203,7 @@ "configured": "yapılandırıldı", "none": "Yok", "modelOverrideValuePlaceholder": "Sayısal değer", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Anahtar değer ekle", "noModelOverrides": "Bu model için yapılandırılmış geçersiz kılma yok.", "modelOverrideLoadFailed": "Model geçersiz kılmaları yüklenemedi", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Kısa ve öz CJK (文言)", "description": "Klasik Çince ultra kısa ve öz stil (yalnızca Çince için kullanılabilir)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Round-robin ve rastgele kombinasyonlar, tüm bir konuşmayı ilk mesaj karmasıyla tek bir bağlantıya sabitlemek yerine her istekte farklı bir bağlantıya döner. Çok turlu sohbetlerde prompt-cache isabetlerini korumak için kapalı bırakın. Kombinasyon bazlı geçersiz kılmalar önceliklidir.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Kimlik Bilgisi Karartma", "credentialRedactionDesc": "Sağlayıcılara gönderilen bağlamdan ve yanıtlardan API anahtarlarını, belirteçleri ve sırları karartın.", "enableCredentialRedaction": "Kimlik bilgisi karartmayı etkinleştir", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Motoru etkinleştir", "enableDescription": "Yığında en son çalışır (RTK/Caveman metni temizledikten sonra OmniGlyph geri kalanını görüntülere dönüştürür) ve ayrıca omniglyph modu aracılığıyla bağımsız olarak çalışır. Bu bir önizlemedir ve uçtan uca doğrulama tamamlanana kadar varsayılan olarak kapalı kalır.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Kaydedildi.", "saveFailed": "Kaydedilemedi.", "enableAria": "OmniGlyph motorunu etkinleştir", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "maksimum", "grokAutoTopUpMonth": "ay", "grokAdditionalCredits": "Ekstra Krediler", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Günlükler", "proxyTab": "Proxy", "budgetManagement": "Bütçe Yönetimi", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "İlk Token", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 9fe5efbd52..ce9ede8d17 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Логи консолі", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Візуальний графік запитів", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Глобальна маршрутизація", "mitmProxy": "MITM-проксі", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "відкрити", "close": "закрити" }, - "noResults": "Немає результатів", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "Немає результатів" }, "webhooks": { "title": "Веб-хуки", @@ -1739,8 +1739,8 @@ "quotaShare": "Частка квоти", "discovery": "Дослідження", "freeProviderRankings": "Рейтинги безкоштовних провайдерів", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "Безкоштовні тарифи", "gamification": "Гейміфікація", "leaderboard": "Таблиця лідерів", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Перевірте та збережіть", "wizardStep4Desc": "Перегляньте конфігурацію та активуйте комбо", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "Цей постачальник більше не підтримується", "riskNotice": { "title": "Перед тим, як продовжити", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "Провайдер із застереженнями щодо використання — натисніть для подробиць", "oauth": "Цей провайдер використовує вашу офіційну продуктову сесію/OAuth, які не авторизовані для використання у проксі/маршрутизаторі. Ми не рекомендуємо інтенсивне автономне використання агентами (стиль OpenCloud, довгі багатокрокові потоки, великі пакети) — провайдер може у відповідь обмежити або заблокувати акаунт. Використовуйте на власний ризик.", "webCookie": "Цей провайдер автентифікується через cookie вашої веб-сесії. Сервіс може в будь-який момент анулювати сесію, що вимагатиме повторного входу. Не рекомендовано для довгих автоматизованих операцій. Використовуйте на власний ризик.", @@ -5107,9 +5111,9 @@ "cancel": "Скасувати" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Вимкнено", "enableProvider": "Увімкнути провайдера", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Пропуск {count} наявних моделей", "autoSync": "Автоматична синхронізація", "autoSyncShort": "Синхронізувати", + "autoFetchModels": "Автоматичне отримання моделей з upstream", + "autoFetchModelsTooltip": "Отримати та кешувати моделі з upstream за потреби", + "autoFetchModelsEnabled": "Увімкнено автоматичне отримання моделі з upstream", + "autoFetchModelsDisabled": "Автоматичне отримання моделі з upstream вимкнено", + "autoFetchModelsToggleFailed": "Не вдалося перемкнути автоматичне отримання моделі upstream", + "autoFetchModelsPartialFailure": "Деякі з'єднання оновлено, але автоматичне отримання моделі з верхнього рівня не було змінено скрізь", + "overridesUpstreamModel": "Перезаписує upstream", + "overridesUpstreamModelHint": "Ваші налаштування переважають цю модель вгору за течією", + "resetToUpstreamDefaults": "Відновити значення за замовчуванням з upstream", + "resetToUpstreamDefaultsSuccess": "Відновлено значення за замовчуванням моделі з upstream", + "resetToUpstreamDefaultsFailed": "Не вдалося відновити значення за замовчуванням моделі upstream", "autoSyncTooltip": "Автоматично оновлювати список моделей кожні 24 години (налаштовується через MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Автоматична синхронізація ввімкнена — моделі періодично оновлюватимуться", "autoSyncDisabled": "Автоматична синхронізація вимкнена", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "Перенаправляти виклики нативного інструмента web_fetch на /v1/web/fetch в OmniRoute.", "interceptionLoadError": "Не вдалося завантажити налаштування перехоплення: {error}", "interceptionSaveError": "Не вдалося зберегти налаштування перехоплення: {error}", - "ccAliasSectionTitle": "Відкрити в Claude Code (claude/…)", - "ccAliasSectionHint": "Рекламуйте моделі цього постачальника під claude/<provider>/<model> mirror ids, щоб модель виявлення шлюзу Claude Code могла їх перерахувати. Вимкнено за замовчуванням — увімкнення цього подвоює записи каталогу для всіх клієнтів.", - "ccAliasProviderLevelLabel": "Постачальник за замовчуванням", - "ccAliasModelOverridesLabel": "Перемикання за моделлю", - "ccAliasModelOverrideAriaLabel": "Перезапис для {modelId}", - "ccAliasStateInherit": "Успадкувати", - "ccAliasStateOn": "Увімкнено", - "ccAliasStateOff": "Вимкнено", - "ccAliasAddModelPlaceholder": "Ідентифікатор моделі (наприклад, gpt-4o)", - "ccAliasAddModelButton": "Додати перевизначення", - "ccAliasLoadError": "Не вдалося завантажити налаштування discovery-alias: {error}", - "ccAliasSaveError": "Не вдалося зберегти налаштування discovery-alias: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Connect Galadriel with an API key.", "predibase": "$25 free trial credits (30-day validity)", "chenzk": "OpenAI-compatible gateway with a live model catalog at chenzk.top.", - "freepik": "Generate images with Freepik's Mystic API.", + "magnific": "Generate images with Freepik's Mystic API.", "freetheai": "Free OpenAI-compatible gateway with passthrough model support.", "g4f-gemini": "Free no-key g4f.space reverse proxy to Gemini, limited to 5 requests per minute.", "g4f-groq": "Free no-key g4f.space reverse proxy to Groq, limited to 5 requests per minute.", @@ -6209,6 +6224,7 @@ "claude": "Connect Claude Code with the existing OAuth flow.", "cline": "Connect Cline with the existing OAuth flow.", "cursor": "Connect Cursor IDE with the existing OAuth flow.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Connect GitHub Copilot with the existing OAuth flow.", "gitlab-duo": "OAuth application with ai_features + read_user scopes. Configure GITLAB_DUO_OAUTH_CLIENT_ID and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET on this OmniRoute instance.", "kilocode": "Connect Kilo Code with the existing OAuth flow.", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "У періоді очікування", "codexPoolUsed": "використано", "codexPoolUntil": "До {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "Анонімний резервний варіант", "anonymousFallbackDesc": "Коли всі налаштовані з'єднання вичерпані (квота, кредити або термін дії), тимчасово використовуйте безключовий рівень цього постачальника. Вимкніть, щоб пропустити цього постачальника замість надсилання анонімних запитів — рекомендовано, коли безключовий рівень їх відхиляє (401).", "anonymousFallbackEnabled": "Анонімний резервний варіант увімкнено для {provider}", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "Налаштування кінцевої точки збереженої моделі", "searchByModelAria": "Пошук за моделлю", "selectSupportedEndpoint": "Виберіть принаймні одну підтримувану точку доступу", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Автоматичне отримання моделей з upstream", - "autoFetchModelsDisabled": "Автоматичне отримання моделі з upstream вимкнено", - "autoFetchModelsTooltip": "Отримати та кешувати моделі з upstream за потреби", - "autoFetchModelsEnabled": "Увімкнено автоматичне отримання моделі з upstream", - "autoFetchModelsToggleFailed": "Не вдалося перемкнути автоматичне отримання моделі upstream", - "overridesUpstreamModel": "Перезаписує upstream", - "overridesUpstreamModelHint": "Ваші налаштування переважають цю модель вгору за течією", - "autoFetchModelsPartialFailure": "Деякі з'єднання оновлено, але автоматичне отримання моделі з верхнього рівня не було змінено скрізь", - "resetToUpstreamDefaultsSuccess": "Відновлено значення за замовчуванням моделі з upstream", - "resetToUpstreamDefaults": "Відновити значення за замовчуванням з upstream", - "resetToUpstreamDefaultsFailed": "Не вдалося відновити значення за замовчуванням моделі upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Налаштування", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "Banned Keywords", "customBannedSignalsDesc": "Додаткові ключові слова, які запускають виявлення постійного блокування акаунта. Вбудовані ключові слова застосовуються завжди.", "customBannedSignalsPlaceholder": "наприклад, api key revoked", @@ -7210,6 +7203,7 @@ "configured": "налаштовано", "none": "Немає", "modelOverrideValuePlaceholder": "Числове значення", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Додати ключ-значення", "noModelOverrides": "Для цієї моделі не налаштовано перевизначень.", "modelOverrideLoadFailed": "Не вдалося завантажити перевизначення моделі", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "Стисла CJK (文言)", "description": "Класичний китайський ультрастислий стиль (доступно тільки для китайської)." @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "Комбінації Round-robin та random перемикаються на інше з'єднання при кожному запиті замість закріплення всієї розмови за одним з'єднанням за хешем першого повідомлення. Залиште вимкненим, щоб зберегти влучання в кеш підказок (prompt-cache) для багатокрокових чатів. Перевизначення для конкретних комбінацій мають пріоритет.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "Вилучення облікових даних", "credentialRedactionDesc": "Вилучати API-ключі, токени та секрети з контексту, що надсилається провайдерам, та з відповідей.", "enableCredentialRedaction": "Увімкнути вилучення облікових даних", @@ -8621,6 +8623,27 @@ }, "enableTitle": "Увімкнути рушій", "enableDescription": "Запускається останнім у стеку (після того, як RTK/Caveman очищає текст, а OmniGlyph конвертує решту в зображення), а також працює автономно в режимі omniglyph. Це попередня версія, яка залишається вимкненою за замовчуванням до завершення наскрізної перевірки.", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "Збережено.", "saveFailed": "Не вдалося зберегти.", "enableAria": "Увімкнути рушій OmniGlyph", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "макс", "grokAutoTopUpMonth": "місяць", "grokAdditionalCredits": "Додаткові Кредити", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Лісоруб", "proxyTab": "Проксі", "budgetManagement": "Управління бюджетом", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "Перший токен", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 76749d9d57..b5c237a7d5 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Console Logs", "logsTimeline": "Timeline", "logsTimelineSubtitle": "بصری درخواست کا وقت لائن", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Global Routing", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "کھولیں", "close": "بند کریں" }, - "noResults": "کوئی نتائج نہیں", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "کوئی نتائج نہیں" }, "webhooks": { "title": "ویب ہکس", @@ -1739,8 +1739,8 @@ "quotaShare": "کوٹہ شیئر", "discovery": "دریافت", "freeProviderRankings": "مفت فراہم کنندگان کی درجہ بندی", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "مفت ٹیئرز", "gamification": "گیمیفیکیشن", "leaderboard": "لیڈر بورڈ", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "Pick how requests are distributed across your models - 13 strategies available", "wizardStep4Title": "Review & Save", "wizardStep4Desc": "Review your configuration and activate the combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "On", "emailVisibilityStateOff": "Off", "reorderHandle": "Drag to reorder", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "اس فراہم کنندہ کو فرسودہ کر دیا گیا ہے۔", "riskNotice": { "title": "جاری رکھنے سے پہلے", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "استعمال کے انتباہات والا فراہم کنندہ — تفصیلات کے لیے کلک کریں", "oauth": "یہ فراہم کنندہ آپ کے آفیشل پروڈکٹ سیشن/OAuth کا استعمال کرتا ہے، جو پراکسی/راؤٹر کے استعمال کے لیے مجاز نہیں ہے۔ ہم خود مختار ایجنٹ کے زیادہ استعمال (OpenCloud طرز، طویل کثیر مرحلہ جاتی فلو، بڑے بیچز) کی سفارش نہیں کرتے ہیں — اپ اسٹریم اکاؤنٹ کو محدود یا بین کر کے ردعمل ظاہر کر سکتا ہے۔ اپنے خطرے پر استعمال کریں۔", "webCookie": "یہ فراہم کنندہ آپ کے ویب سیشن کوکیز کے ذریعے توثیق کرتا ہے۔ اپ اسٹریم سروس کسی بھی وقت سیشن کو باطل کر سکتی ہے، جس کے لیے آپ کو دوبارہ لاگ ان کرنے کی ضرورت ہوگی۔ طویل غیر حاضر کارروائیوں کے لیے تجویز نہیں کی جاتی ہے۔ اپنے خطرے پر استعمال کریں۔", @@ -5107,9 +5111,9 @@ "cancel": "منسوخ کریں" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "Disabled", "enableProvider": "Enable provider", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "Skipping {count} existing models", "autoSync": "Auto-Sync", "autoSyncShort": "Sync", + "autoFetchModels": "خودکار طور پر اپ اسٹریم ماڈلز حاصل کریں", + "autoFetchModelsTooltip": "جب ضرورت ہو تو اوپر کے ماڈلز کو حاصل کریں اور کیش کریں", + "autoFetchModelsEnabled": "اپ اسٹریم ماڈل خودکار حاصل کرنا فعال ہے", + "autoFetchModelsDisabled": "اپ اسٹریم ماڈل خودکار حاصل کرنا غیر فعال ہے", + "autoFetchModelsToggleFailed": "اپ اسٹریم ماڈل خودکار حاصل کرنے کو تبدیل کرنے میں ناکامی", + "autoFetchModelsPartialFailure": "کچھ کنکشنز کو اپ ڈیٹ کیا گیا، لیکن اوپر کی طرف ماڈل خودکار طور پر ہر جگہ تبدیل نہیں ہوا", + "overridesUpstreamModel": "اوپر والے کو اووررائیڈ کرتا ہے", + "overridesUpstreamModelHint": "آپ کی ترتیبات اس اوپر کی ماڈل کو اووررائیڈ کرتی ہیں", + "resetToUpstreamDefaults": "اپ اسٹریم ڈیفالٹس بحال کریں", + "resetToUpstreamDefaultsSuccess": "اپ اسٹریم ماڈل کے ڈیفالٹس بحال کر دیے گئے ہیں", + "resetToUpstreamDefaultsFailed": "اپ اسٹریم ماڈل کے ڈیفالٹس کو بحال کرنے میں ناکامی", "autoSyncTooltip": "Automatically refresh model list every 24h (configurable via MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Auto-sync enabled — models will refresh periodically", "autoSyncDisabled": "Auto-sync disabled", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "نیٹو web_fetch ٹول کالز کو OmniRoute کے /v1/web/fetch پر دوبارہ لکھیں۔", "interceptionLoadError": "انٹرسیپشن کی ترتیبات لوڈ کرنے میں ناکامی: {error}", "interceptionSaveError": "انٹرسیپشن کی ترتیبات محفوظ کرنے میں ناکامی: {error}", - "ccAliasSectionTitle": "Claude کو کوڈ میں ظاہر کریں (claude/…)", - "ccAliasSectionHint": "اس فراہم کنندہ کے ماڈلز کو claude/<provider>/<model> آئینہ شناختوں کے تحت اشتہار دیں تاکہ Claude Code کے گیٹ وے ماڈل کی دریافت انہیں درج کر سکے۔ ڈیفالٹ کے طور پر بند — اس کو فعال کرنے سے تمام کلائنٹس کے لیے کیٹلاگ کی اندراجات دوگنا ہو جاتی ہیں۔", - "ccAliasProviderLevelLabel": "فراہم کنندہ ڈیفالٹ", - "ccAliasModelOverridesLabel": "فی ماڈل اووررائیڈز", - "ccAliasModelOverrideAriaLabel": "{modelId} کے لیے اووررائیڈ", - "ccAliasStateInherit": "وراثت", - "ccAliasStateOn": "پر", - "ccAliasStateOff": "بند", - "ccAliasAddModelPlaceholder": "ماڈل آئی ڈی (جیسے gpt-4o)", - "ccAliasAddModelButton": "اووررائیڈ شامل کریں", - "ccAliasLoadError": "ڈسکوری-ایلیاس سیٹنگز لوڈ کرنے میں ناکامی: {error}", - "ccAliasSaveError": "ڈسکوری-ایلیاس سیٹنگ کو محفوظ کرنے میں ناکامی: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "Extra upstream headers", "compatUpstreamHeadersHint": "High-privilege setting — same trust level as editing provider API credentials; only trusted admins should use it. Merged after OmniRoute adds auth from the provider API key. If a custom header uses the same name as an existing one (e.g. Authorization), your value fully replaces the auto-generated header (including the Bearer token) — the upstream only sees what you typed, not the key from settings. Misconfiguration can cause 401 or broken upstream auth. One row per header (e.g. extra Authentication for some gateways). Hover or focus the value to preview. Saves on blur, outside click, or closing this panel.", "compatUpstreamHeaderName": "Header name", @@ -6194,7 +6209,7 @@ "galadriel": "Galadriel کو ایک API کلید کے ساتھ منسلک کریں۔", "predibase": "$25 کے مفت ٹرائل کریڈٹس (30 دن کی میعاد)", "chenzk": "chenzk.top پر لائیو ماڈل کیٹلاگ کے ساتھ OpenAI سے مطابقت رکھنے والا گیٹ وے۔", - "freepik": "Freepik کے Mystic API کے ساتھ تصاویر تیار کریں۔", + "magnific": "Freepik کے Mystic API کے ساتھ تصاویر تیار کریں۔", "freetheai": "passthrough ماڈل سپورٹ کے ساتھ مفت OpenAI سے مطابقت رکھنے والا گیٹ وے۔", "g4f-gemini": "Gemini کے لیے مفت بغیر کلید والا g4f.space ریورس پراکسی، فی منٹ 5 درخواستوں تک محدود۔", "g4f-groq": "Groq کے لیے مفت بغیر کلید والا g4f.space ریورس پراکسی، فی منٹ 5 درخواستوں تک محدود۔", @@ -6209,6 +6224,7 @@ "claude": "Claude Code کو موجودہ OAuth فلو کے ساتھ منسلک کریں۔", "cline": "Cline کو موجودہ OAuth فلو کے ساتھ منسلک کریں۔", "cursor": "Cursor IDE کو موجودہ OAuth فلو کے ساتھ منسلک کریں۔", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "GitHub Copilot کو موجودہ OAuth فلو کے ساتھ منسلک کریں۔", "gitlab-duo": "ai_features + read_user اسکوپس کے ساتھ OAuth ایپلیکیشن۔ اس OmniRoute انسٹنس پر GITLAB_DUO_OAUTH_CLIENT_ID اور اختیاری طور پر GITLAB_DUO_OAUTH_CLIENT_SECRET کنفیگر کریں۔", "kilocode": "Kilo Code کو موجودہ OAuth فلو کے ساتھ منسلک کریں۔", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "وقفۂ انتظار میں", "codexPoolUsed": "استعمال شدہ", "codexPoolUntil": "{value} تک", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "نامعلوم متبادل", "anonymousFallbackDesc": "جب تمام کنفیگر کردہ کنکشنز ختم ہو جائیں (کوٹہ، کریڈٹس، یا میعاد)، اس فراہم کنندہ کی بغیر کلید کی سطح کو عارضی طور پر استعمال کریں۔ اس فراہم کنندہ کو چھوڑنے کے لیے بند کریں بجائے اس کے کہ گمنام درخواستیں بھیجیں — جب بغیر کلید کی سطح انہیں مسترد کرتی ہے (401) تو یہ تجویز کردہ ہے۔", "anonymousFallbackEnabled": "{provider} کے لیے نامعلوم متبادل فعال ہے", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "محفوظ شدہ ماڈل اینڈپوائنٹ کی ترتیبات", "searchByModelAria": "ماڈل کے ذریعے تلاش کریں", "selectSupportedEndpoint": "کم از کم ایک سپورٹ کردہ اینڈپوائنٹ منتخب کریں", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModelsDisabled": "اپ اسٹریم ماڈل خودکار حاصل کرنا غیر فعال ہے", - "autoFetchModelsEnabled": "اپ اسٹریم ماڈل خودکار حاصل کرنا فعال ہے", - "autoFetchModelsTooltip": "جب ضرورت ہو تو اوپر کے ماڈلز کو حاصل کریں اور کیش کریں", - "autoFetchModels": "خودکار طور پر اپ اسٹریم ماڈلز حاصل کریں", - "autoFetchModelsToggleFailed": "اپ اسٹریم ماڈل خودکار حاصل کرنے کو تبدیل کرنے میں ناکامی", - "overridesUpstreamModel": "اوپر والے کو اووررائیڈ کرتا ہے", - "autoFetchModelsPartialFailure": "کچھ کنکشنز کو اپ ڈیٹ کیا گیا، لیکن اوپر کی طرف ماڈل خودکار طور پر ہر جگہ تبدیل نہیں ہوا", - "overridesUpstreamModelHint": "آپ کی ترتیبات اس اوپر کی ماڈل کو اووررائیڈ کرتی ہیں", - "resetToUpstreamDefaultsFailed": "اپ اسٹریم ماڈل کے ڈیفالٹس کو بحال کرنے میں ناکامی", - "resetToUpstreamDefaultsSuccess": "اپ اسٹریم ماڈل کے ڈیفالٹس بحال کر دیے گئے ہیں", - "resetToUpstreamDefaults": "اپ اسٹریم ڈیفالٹس بحال کریں" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Settings", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.", "autoDisableThreshold": "Ban Threshold", "autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "ممنوعہ الفاظ", "customBannedSignalsDesc": "اضافی الفاظ جو مستقل اکاؤنٹ پر پابندی کی شناخت کو متحرک کرتے ہیں۔ پہلے سے موجود الفاظ ہمیشہ لاگو ہوتے ہیں۔", "customBannedSignalsPlaceholder": "مثال کے طور پر api key revoked", @@ -7210,6 +7203,7 @@ "configured": "کنفیگر شدہ", "none": "کوئی نہیں", "modelOverrideValuePlaceholder": "عددی قدر", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "کی ویلیو شامل کریں", "noModelOverrides": "اس ماڈل کے لیے کوئی اوور رائیڈز کنفیگر نہیں کیے گئے۔", "modelOverrideLoadFailed": "ماڈل اوور رائیڈز لوڈ کرنے میں ناکامی", @@ -7781,6 +7775,10 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, + "i-have-adhd": { + "label": "I have ADHD (action-first)", + "description": "Action-first output: next action leads, steps numbered, one concrete next step, no preamble." + }, "terse-cjk": { "label": "مختصر CJK (文言)", "description": "کلاسیکی چینی انتہائی مختصر انداز (صرف چینی زبان کے لیے دستیاب ہے)۔" @@ -8061,6 +8059,10 @@ "disableSessionStickinessDesc": "راؤنڈ رابن اور رینڈم کمبوز پہلے پیغام کے ہیش کے ذریعے پوری گفتگو کو ایک کنکشن سے منسلک کرنے کے بجائے ہر درخواست پر ایک مختلف کنکشن پر منتقل ہو جاتے ہیں۔ ملٹی ٹرن چیٹس کے لیے پرامپٹ کیش ہٹس کو برقرار رکھنے کے لیے اسے بند رہنے دیں۔ فی کمبو اوور رائیڈز کو ترجیح حاصل ہوگی۔", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Reasoning token buffer", + "reasoningTokenBufferDesc": "Allow combo routing to add max_tokens headroom only for known reasoning models when the full buffer fits inside a known output cap.", + "zeroLatencyOptimizations": "Zero-latency optimizations", + "zeroLatencyOptimizationsDesc": "Opt in to hedging, predictive TTFT skips, and proactive fallback compression. Leave off to prevent these latency features from racing targets or compressing fallback requests.", "credentialRedaction": "اسناد کی ریڈیکشن", "credentialRedactionDesc": "فراہم کنندگان کو بھیجے گئے سیاق و سباق اور جوابات سے API کیز، ٹوکنز اور خفیہ معلومات کو سنسر کریں۔", "enableCredentialRedaction": "اسناد کی سنسرشپ کو فعال کریں", @@ -8621,6 +8623,27 @@ }, "enableTitle": "انجن فعال کریں", "enableDescription": "اسٹیک میں سب سے آخر میں چلتا ہے (RTK/Caveman کے متن صاف کرنے کے بعد، OmniGlyph باقی ماندہ کو تصاویر میں تبدیل کرتا ہے) اور omniglyph موڈ کے ذریعے اسٹینڈ الون بھی چلتا ہے۔ یہ ایک پیش نظارہ ہے اور اینڈ ٹو اینڈ توثیق مکمل ہونے تک ڈیفالٹ طور پر بند رہتا ہے۔", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "محفوظ ہو گیا۔", "saveFailed": "محفوظ نہیں ہو سکا۔", "enableAria": "OmniGlyph انجن فعال کریں", @@ -9090,6 +9113,16 @@ "grokAutoTopUpMax": "زیادہ سے زیادہ", "grokAutoTopUpMonth": "مہینہ", "grokAdditionalCredits": "اضافی کریڈٹس", + "kimiExtraUsageCredits": "Extra Usage Credits", + "kimiExtraUsage": "Extra Usage", + "kimiExtraUsageEnabled": "Enabled", + "kimiExtraUsageDisabled": "Disabled", + "kimiExtraUsageFrozen": "Frozen", + "kimiExtraUsageUnavailable": "Unavailable", + "kimiMonthlyUsed": "Used this month", + "kimiMonthlyLimit": "Monthly limit", + "kimiMonthlyLimitUnlimited": "Unlimited", + "kimiAdditionalCredits": "Additional Credits", "loggerTab": "Logger", "proxyTab": "Proxy", "budgetManagement": "Budget Management", @@ -12488,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "پہلا ٹوکن", @@ -13213,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13753,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index b4ba071acd..9cf64c0d2f 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -1165,6 +1165,8 @@ "consoleLogs": "Nhật ký bảng điều khiển", "logsTimeline": "Timeline", "logsTimelineSubtitle": "Dòng thời gian yêu cầu trực quan", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "Định tuyến toàn cục", "mitmProxy": "MITM Proxy", "oneProxy": "1Proxy", @@ -1282,8 +1284,6 @@ "resilienceConnectionsSubtitle": "Cooldown, cầu dao, trạng thái khóa", "settingsModalityBridge": "Cầu Kết Nối Modality", "settingsModalityBridgeSubtitle": "Chuyển đổi hình ảnh/âm thanh → văn bản cho các mô hình chỉ văn bản", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations", "commandPalette": { "title": "Bảng lệnh", "searchPlaceholder": "Tìm kiếm trang, cài đặt, công cụ...", @@ -1800,6 +1800,11 @@ "updateStarted": "Đã bắt đầu cập nhật...", "reloadingPageAutomatically": "Đang tự động tải lại trang...", "providerTopology": "Cấu trúc liên kết nhà cung cấp", + "recentRequests": "Yêu cầu gần đây", + "recentRequestsEmpty": "Chưa có yêu cầu nào.", + "recentRequestsModel": "Mô hình", + "recentRequestsTokens": "Vào / Ra", + "recentRequestsWhen": "Khi", "downloadDmg": "Tải xuống DMG (macOS)", "downloadDmgDescription": "Đã có phiên bản mới của ứng dụng máy tính OmniRoute. Vui lòng tải xuống và cài đặt trình cài đặt DMG cho macOS để cập nhật (hiện tại: v{version}).", "downloadExe": "Tải xuống EXE (Windows)", @@ -3606,6 +3611,10 @@ "wizardStep3Desc": "Chọn cách phân phối các yêu cầu giữa các mô hình của bạn - hiện có 14 chiến lược", "wizardStep4Title": "Xem lại & Lưu", "wizardStep4Desc": "Xem lại cấu hình của bạn và kích hoạt combo", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "Bật", "emailVisibilityStateOff": "Tắt", "reorderHandle": "Kéo để sắp xếp lại", @@ -5233,6 +5242,17 @@ "skippingExistingModels": "Bỏ qua {count} mô hình đã tồn tại", "autoSync": "Tự động đồng bộ hóa", "autoSyncShort": "Đồng bộ", + "autoFetchModels": "Tự động lấy các mô hình upstream", + "autoFetchModelsTooltip": "Lấy và lưu trữ các mô hình upstream khi cần thiết", + "autoFetchModelsEnabled": "Mô hình upstream tự động lấy dữ liệu đã được kích hoạt", + "autoFetchModelsDisabled": "Tự động lấy mô hình upstream đã bị vô hiệu hóa", + "autoFetchModelsToggleFailed": "Không thể chuyển đổi chế độ tự động lấy mô hình upstream", + "autoFetchModelsPartialFailure": "Một số kết nối đã được cập nhật, nhưng mô hình upstream auto-fetch không được thay đổi ở mọi nơi", + "overridesUpstreamModel": "Ghi đè lên upstream", + "overridesUpstreamModelHint": "Cài đặt của bạn ghi đè lên mô hình upstream này", + "resetToUpstreamDefaults": "Khôi phục mặc định của upstream", + "resetToUpstreamDefaultsSuccess": "Đã khôi phục các giá trị mặc định của mô hình upstream", + "resetToUpstreamDefaultsFailed": "Không thể khôi phục mặc định mô hình upstream", "autoSyncTooltip": "Tự động làm mới danh sách mô hình sau mỗi 24 giờ (có thể cấu hình qua MODEL_SYNC_INTERVAL_HOURS)", "autoSyncEnabled": "Đã bật tự động đồng bộ hóa — các mô hình sẽ được làm mới định kỳ", "autoSyncDisabled": "Đã tắt tự động đồng bộ hóa", @@ -6194,7 +6214,7 @@ "galadriel": "Kết nối Galadriel bằng khóa API.", "predibase": "25 USD tín dụng dùng thử miễn phí (có hiệu lực 30 ngày)", "chenzk": "Gateway tương thích OpenAI với danh mục mô hình trực tiếp tại chenzk.top.", - "freepik": "Tạo hình ảnh bằng API Mystic của Freepik.", + "magnific": "Tạo hình ảnh bằng API Mystic của Freepik.", "freetheai": "Gateway miễn phí tương thích OpenAI, hỗ trợ chuyển tiếp mô hình.", "g4f-gemini": "Proxy ngược g4f.space miễn phí, không cần khóa cho Gemini, giới hạn 5 yêu cầu mỗi phút.", "g4f-groq": "Proxy ngược g4f.space miễn phí, không cần khóa cho Groq, giới hạn 5 yêu cầu mỗi phút.", @@ -6209,6 +6229,7 @@ "claude": "Kết nối Claude Code bằng luồng OAuth hiện có.", "cline": "Kết nối Cline bằng luồng OAuth hiện có.", "cursor": "Kết nối Cursor IDE bằng luồng OAuth hiện có.", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "Kết nối GitHub Copilot bằng luồng OAuth hiện có.", "gitlab-duo": "Ứng dụng OAuth với phạm vi ai_features + read_user. Cấu hình GITLAB_DUO_OAUTH_CLIENT_ID và tùy chọn GITLAB_DUO_OAUTH_CLIENT_SECRET trên instance OmniRoute này.", "kilocode": "Kết nối Kilo Code bằng luồng OAuth hiện có.", @@ -6280,18 +6301,6 @@ "codexPoolCoolingDown": "Đang trong thời gian chờ", "codexPoolUsed": "đã dùng", "codexPoolUntil": "Đến {value}", - "ccAliasSectionTitle": "Hiển thị trong Claude Code (claude/…)", - "ccAliasSectionHint": "Công bố các mô hình của nhà cung cấp này dưới dạng id phản chiếu claude/<provider>/<model> để tính năng khám phá mô hình qua gateway của Claude Code có thể liệt kê chúng. Mặc định tắt — bật lên sẽ nhân đôi số mục trong danh mục với mọi client.", - "ccAliasProviderLevelLabel": "Mặc định của nhà cung cấp", - "ccAliasModelOverridesLabel": "Ghi đè theo từng mô hình", - "ccAliasModelOverrideAriaLabel": "Ghi đè cho {modelId}", - "ccAliasStateInherit": "Kế thừa", - "ccAliasStateOn": "Bật", - "ccAliasStateOff": "Tắt", - "ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)", - "ccAliasAddModelButton": "Thêm ghi đè", - "ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}", - "ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}", "anonymousFallbackTitle": "Dự phòng ẩn danh", "anonymousFallbackDesc": "Khi tất cả kết nối đã cấu hình đều cạn kiệt (hạn ngạch, tín dụng hoặc hết hạn), hãy tạm thời sử dụng tầng không cần khóa của nhà cung cấp này. Tắt tùy chọn này để bỏ qua nhà cung cấp thay vì gửi yêu cầu ẩn danh — khuyến nghị khi tầng không cần khóa từ chối các yêu cầu đó (401).", "anonymousFallbackEnabled": "Đã bật dự phòng ẩn danh cho {provider}", @@ -6367,18 +6376,7 @@ "savedModelEndpointSettings": "Đã lưu cài đặt endpoint mô hình", "searchByModelAria": "Tìm kiếm theo mô hình", "selectSupportedEndpoint": "Chọn ít nhất một endpoint được hỗ trợ", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "Tự động lấy các mô hình upstream", - "autoFetchModelsDisabled": "Tự động lấy mô hình upstream đã bị vô hiệu hóa", - "autoFetchModelsEnabled": "Mô hình upstream tự động lấy dữ liệu đã được kích hoạt", - "autoFetchModelsTooltip": "Lấy và lưu trữ các mô hình upstream khi cần thiết", - "overridesUpstreamModel": "Ghi đè lên upstream", - "autoFetchModelsPartialFailure": "Một số kết nối đã được cập nhật, nhưng mô hình upstream auto-fetch không được thay đổi ở mọi nơi", - "overridesUpstreamModelHint": "Cài đặt của bạn ghi đè lên mô hình upstream này", - "autoFetchModelsToggleFailed": "Không thể chuyển đổi chế độ tự động lấy mô hình upstream", - "resetToUpstreamDefaults": "Khôi phục mặc định của upstream", - "resetToUpstreamDefaultsSuccess": "Đã khôi phục các giá trị mặc định của mô hình upstream", - "resetToUpstreamDefaultsFailed": "Không thể khôi phục mặc định mô hình upstream" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "Cài đặt", @@ -7210,6 +7208,7 @@ "configured": "đã định cấu hình", "none": "Không có", "modelOverrideValuePlaceholder": "Giá trị số", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "Thêm cặp khóa-giá trị", "noModelOverrides": "Không có ghi đè nào được định cấu hình cho mô hình này.", "modelOverrideLoadFailed": "Tải ghi đè mô hình thất bại", @@ -7781,13 +7780,13 @@ "label": "Ponytail (lazy senior dev)", "description": "Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff." }, - "terse-cjk": { - "label": "CJK súc tích (文言)", - "description": "Văn phong Hán cổ cực kỳ súc tích (chỉ khả dụng với tiếng Trung)." - }, "i-have-adhd": { "label": "Tôi bị ADHD (ưu tiên hành động)", "description": "Đầu ra ưu tiên hành động: nêu hành động kế tiếp trước, các bước được đánh số, một bước tiếp theo cụ thể, không mở đầu dài dòng." + }, + "terse-cjk": { + "label": "CJK súc tích (文言)", + "description": "Văn phong Hán cổ cực kỳ súc tích (chỉ khả dụng với tiếng Trung)." } }, "resilienceWaitForCooldown": "Chờ thời gian hồi", @@ -8065,6 +8064,10 @@ "disableSessionStickinessDesc": "Các tổ hợp luân phiên và ngẫu nhiên sẽ chuyển sang một kết nối khác với mỗi yêu cầu, thay vì gắn toàn bộ cuộc trò chuyện với một kết nối dựa trên mã băm của tin nhắn đầu tiên. Hãy để tùy chọn này tắt để duy trì các lần trúng cache prompt cho các cuộc trò chuyện nhiều lượt. Các thiết lập ghi đè theo từng tổ hợp được ưu tiên.", "promptCacheAffinity": "Prompt-cache locality routing", "promptCacheAffinityDesc": "Prefer the same provider account for matching prompt-cache keys while preserving health and quota failover.", + "reasoningTokenBuffer": "Bộ đệm token suy luận", + "reasoningTokenBufferDesc": "Cho phép định tuyến combo thêm khoảng dư max_tokens chỉ với các mô hình suy luận đã biết, khi toàn bộ bộ đệm vẫn nằm trong giới hạn đầu ra đã biết.", + "zeroLatencyOptimizations": "Tối ưu hóa zero-latency", + "zeroLatencyOptimizationsDesc": "Bật hedging, bỏ qua TTFT theo dự đoán và nén dự phòng chủ động. Để tắt nếu bạn không muốn các tính năng độ trễ này chạy đua giữa các đích hoặc nén các yêu cầu dự phòng.", "credentialRedaction": "Credential Redaction", "credentialRedactionDesc": "Redact API keys, tokens, and secrets from context sent to providers and from responses.", "enableCredentialRedaction": "Enable credential redaction", @@ -8220,11 +8223,7 @@ "cliproxyapiHealth": "Sức Khỏe", "cliproxyapiPort": "Cổng", "qdrantHost": "Máy chủ", - "qdrantCollection": "Bộ Sưu Tập", - "reasoningTokenBuffer": "Bộ đệm token suy luận", - "reasoningTokenBufferDesc": "Cho phép định tuyến combo thêm khoảng dư max_tokens chỉ với các mô hình suy luận đã biết, khi toàn bộ bộ đệm vẫn nằm trong giới hạn đầu ra đã biết.", - "zeroLatencyOptimizations": "Tối ưu hóa zero-latency", - "zeroLatencyOptimizationsDesc": "Bật hedging, bỏ qua TTFT theo dự đoán và nén dự phòng chủ động. Để tắt nếu bạn không muốn các tính năng độ trễ này chạy đua giữa các đích hoặc nén các yêu cầu dự phòng." + "qdrantCollection": "Bộ Sưu Tập" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index b2c15380aa..b4b6bad59b 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1165,6 +1165,8 @@ "consoleLogs": "控制台日志", "logsTimeline": "时间线", "logsTimelineSubtitle": "可视化请求时间线", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "全局路由", "mitmProxy": "MITM 代理", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "打开", "close": "关闭" }, - "noResults": "没有结果", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "没有结果" }, "webhooks": { "title": "Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "配额共享", "discovery": "发现", "freeProviderRankings": "免费服务商排行", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "免费层级", "gamification": "游戏化", "leaderboard": "排行榜", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "选择请求在模型之间的分发方式 — 提供 13 种策略", "wizardStep4Title": "审查并保存", "wizardStep4Desc": "审查您的配置并激活组合", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "开启", "emailVisibilityStateOff": "关闭", "reorderHandle": "拖拽排序", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "此提供者已弃用", "riskNotice": { "title": "继续之前", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "该提供者有使用注意事项 —— 点击查看详情", "oauth": "此提供者使用你官方产品的会话 / OAuth,这并未被授权用于代理或路由用途。 不建议进行高强度的自主代理使用(OpenCloud 风格、长链路多步流程、大批量请求)—— 上游可能因此限制甚至封禁账号。 使用风险自负。", "webCookie": "此提供者通过你的网页会话 Cookie 进行鉴权。上游服务可能随时让会话失效,届时你需要重新登录。不建议用于长时间无人值守的操作。 使用风险自负。", @@ -5107,9 +5111,9 @@ "cancel": "取消" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "已禁用", "enableProvider": "启用提供者", @@ -5449,18 +5453,18 @@ "interceptFetchHint": "将原生的 web_fetch 工具调用重写为 OmniRoute 的 /v1/web/fetch。", "interceptionLoadError": "加载拦截设置失败:{error}", "interceptionSaveError": "保存拦截设置失败:{error}", - "ccAliasSectionTitle": "在 Claude Code 中暴露 (claude/…)", - "ccAliasSectionHint": "将该提供者的模型以 claude/<provider>/<model> 镜像 ID 发布,让 Claude Code 的网关模型可以发现它们。默认关闭 — 启用会使所有客户端的目录条目翻倍。", - "ccAliasProviderLevelLabel": "提供者默认值", - "ccAliasModelOverridesLabel": "按模型覆盖", - "ccAliasModelOverrideAriaLabel": "覆盖 {modelId}", - "ccAliasStateInherit": "继承", - "ccAliasStateOn": "开启", - "ccAliasStateOff": "关闭", - "ccAliasAddModelPlaceholder": "模型 ID (例如 gpt-4o)", - "ccAliasAddModelButton": "添加覆盖", - "ccAliasLoadError": "加载发现别名设置失败: {error}", - "ccAliasSaveError": "保存发现别名设置失败: {error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "上游额外请求头", "compatUpstreamHeadersHint": "与修改厂商连接/API 配置同属高权限能力,仅可信管理员应使用。这些头会在 OmniRoute 按厂商 API Key 自动加好鉴权头之后再合并。若「名称」与系统已加的头相同(例如都叫 Authorization),则以你填的值为准,会整段替换自动那条(含 Bearer 令牌),上游请求里不再使用面板里保存的密钥来生成 Authorization。填错可能导致 401,请谨慎。每个请求头单独一行;部分网关需要额外 Authentication 等可在此加。鼠标移入或聚焦「值」可暂时看明文。点空白处、关闭本面板或切走焦点即保存。", "compatUpstreamHeaderName": "请求头名称", @@ -6205,7 +6209,7 @@ "galadriel": "使用 API 密钥连接 Galadriel。", "predibase": "$25 免费试用额度(30 天有效期)", "chenzk": "兼容 OpenAI 的网关,在 chenzk.top 提供实时模型目录。", - "freepik": "使用 Freepik 的 Mystic API 生成图像。", + "magnific": "使用 Freepik 的 Mystic API 生成图像。", "freetheai": "免费的 OpenAI 兼容网关,支持直通模型。", "g4f-gemini": "免费免密钥的 g4f.space Gemini 反向代理,限制为每分钟 5 次请求。", "g4f-groq": "免费免密钥的 g4f.space Groq 反向代理,限制为每分钟 5 次请求。", @@ -6220,6 +6224,7 @@ "claude": "使用现有的 OAuth 流程连接 Claude Code。", "cline": "使用现有的 OAuth 流程连接 Cline。", "cursor": "使用现有的 OAuth 流程连接 Cursor IDE。", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "使用现有的 OAuth 流程连接 GitHub Copilot。", "gitlab-duo": "具有 ai_features + read_user 作用域的 OAuth 应用程序。在此 OmniRoute 实例上配置 GITLAB_DUO_OAUTH_CLIENT_ID 以及可选的 GITLAB_DUO_OAUTH_CLIENT_SECRET。", "kilocode": "使用现有的 OAuth 流程连接 Kilo Code。", @@ -6291,18 +6296,6 @@ "codexPoolCoolingDown": "冷却中", "codexPoolUsed": "已使用", "codexPoolUntil": "截至 {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "匿名回退", "anonymousFallbackDesc": "当所有配置的连接耗尽(配额、积分或到期)时,临时使用此提供者的无密钥层。关闭以跳过此提供者,而不是发送匿名请求 — 当无密钥层拒绝它们时(401)建议使用。", "anonymousFallbackEnabled": "为 {provider} 启用匿名回退", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "若提供者连接返回特定的永久封禁信号(如 HTTP 403\"请验证您的账户\"),则将其永久标记为停用。这会将其从组合轮换中移除。", "autoDisableThreshold": "封禁阈值", "autoDisableThresholdDesc": "触发永久停用所需的连续封禁信号次数。", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "封禁关键词", "customBannedSignalsDesc": "触发永久封号检测的附加关键词。内置关键词始终生效。", "customBannedSignalsPlaceholder": "例如 api key revoked", @@ -7210,6 +7203,7 @@ "configured": "已配置", "none": "无", "modelOverrideValuePlaceholder": "数字值", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "添加键值", "noModelOverrides": "该模型尚未配置覆盖。", "modelOverrideLoadFailed": "加载模型覆盖失败", @@ -8629,6 +8623,27 @@ }, "enableTitle": "启用引擎", "enableDescription": "在堆栈中最后运行(在 RTK/Caveman 清理文本、OmniGlyph 将剩余部分转换为图像之后),也可以通过 omniglyph 模式独立运行。此功能为预览版,在端到端验证完成前默认保持关闭。", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "已保存。", "saveFailed": "无法保存。", "enableAria": "启用 OmniGlyph 引擎", @@ -12506,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "首个 Token", @@ -13231,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13771,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 0da35d19db..b2d9679086 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1165,6 +1165,8 @@ "consoleLogs": "控制台日誌", "logsTimeline": "Timeline", "logsTimelineSubtitle": "視覺請求時間線", + "conversations": "Conversations", + "conversationsSubtitle": "Multi-turn agentic conversations", "globalRouting": "全域性路由", "mitmProxy": "MITM 代理", "oneProxy": "1Proxy", @@ -1291,9 +1293,7 @@ "open": "打開", "close": "關閉" }, - "noResults": "沒有結果", - "conversations": "Conversations", - "conversationsSubtitle": "Multi-turn agentic conversations" + "noResults": "沒有結果" }, "webhooks": { "title": "Webhook", @@ -1739,8 +1739,8 @@ "quotaShare": "配額分享", "discovery": "探索", "freeProviderRankings": "免費提供者排名", - "radar": "__MISSING__:Radar", - "setup": "__MISSING__:Setup", + "radar": "Radar", + "setup": "Setup", "freeTiers": "免費方案", "gamification": "遊戲化", "leaderboard": "排行榜", @@ -3606,6 +3606,10 @@ "wizardStep3Desc": "選擇請求在模型之間的分發方式 — 提供 13 種策略", "wizardStep4Title": "審查並儲存", "wizardStep4Desc": "審查您的設定並啟用組合", + "usageGuideInvokeTitle": "How to call this combo", + "usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).", + "usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).", + "usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.", "emailVisibilityStateOn": "開啟", "emailVisibilityStateOff": "關閉", "reorderHandle": "拖拽排序", @@ -5097,7 +5101,7 @@ "deprecatedProvider": "此提供者已棄用", "riskNotice": { "title": "繼續之前", - "detailsTitle": "__MISSING__:Usage caveats", + "detailsTitle": "Usage caveats", "tooltip": "該提供者有使用注意事項 —— 點選檢視詳情", "oauth": "此提供者使用你官方產品的會話 / OAuth,這並未被授權用於代理或路由用途。 不建議進行高強度的自主代理使用(OpenCloud 風格、長鏈路多步流程、大批次請求)—— 上游可能因此限制甚至封禁帳號。 使用風險自負。", "webCookie": "此提供者通過你的網頁會話 Cookie 進行鑑權。上游服務可能隨時讓會話失效,屆時你需要重新登入。不建議用於長時間無人值守的操作。 使用風險自負。", @@ -5107,9 +5111,9 @@ "cancel": "取消" }, "warningNotice": { - "tooltip": "__MISSING__:{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", - "lastFailureSuffix": "__MISSING__: (last failure {time})", - "ariaLabel": "__MISSING__:View connection health details, {count} warning(s)" + "tooltip": "{count} connection(s) flagged — up to {maxFailures} recent failures{lastFailureSuffix}. Click to view connection health.", + "lastFailureSuffix": " (last failure {time})", + "ariaLabel": "View connection health details, {count} warning(s)" }, "disabled": "已停用", "enableProvider": "啟用提供者", @@ -5233,6 +5237,17 @@ "skippingExistingModels": "跳過 {count} 個已有模型", "autoSync": "自動同步", "autoSyncShort": "同步", + "autoFetchModels": "自動獲取上游模型", + "autoFetchModelsTooltip": "在需要時獲取並快取上游模型", + "autoFetchModelsEnabled": "上游模型自動獲取已啟用", + "autoFetchModelsDisabled": "上游模型自動獲取已禁用", + "autoFetchModelsToggleFailed": "無法切換上游模型自動獲取", + "autoFetchModelsPartialFailure": "某些連接已更新,但上游模型自動獲取並未在所有地方更改", + "overridesUpstreamModel": "覆蓋上游", + "overridesUpstreamModelHint": "您的設定覆蓋了此上游模型", + "resetToUpstreamDefaults": "恢復上游預設值", + "resetToUpstreamDefaultsSuccess": "已恢復上游模型預設值", + "resetToUpstreamDefaultsFailed": "無法恢復上游模型的預設值", "autoSyncTooltip": "每 24 小時自動重新整理模型列表(可通過 MODEL_SYNC_INTERVAL_HOURS 設定)", "autoSyncEnabled": "自動同步已啟用 — 模型將定期重新整理", "autoSyncDisabled": "自動同步已停用", @@ -5438,18 +5453,18 @@ "interceptFetchHint": "將原生的 web_fetch 工具呼叫重新導向至 OmniRoute 的 /v1/web/fetch。", "interceptionLoadError": "載入攔截設定失敗:{error}", "interceptionSaveError": "儲存攔截設定失敗:{error}", - "ccAliasSectionTitle": "在 Claude Code (claude/…) 中公開", - "ccAliasSectionHint": "在 claude/<provider>/<model> 鏡像 ID 下廣告此提供者的模型,以便 Claude Code 的網關模型發現可以列出它們。預設為關閉 — 啟用此功能會使所有客戶的目錄條目加倍。", - "ccAliasProviderLevelLabel": "提供者預設", - "ccAliasModelOverridesLabel": "每個模型的覆蓋設定", - "ccAliasModelOverrideAriaLabel": "{modelId} 的覆蓋設定", - "ccAliasStateInherit": "繼承", - "ccAliasStateOn": "開啟", - "ccAliasStateOff": "關閉", - "ccAliasAddModelPlaceholder": "模型 ID(例如:gpt-4o)", - "ccAliasAddModelButton": "新增覆蓋", - "ccAliasLoadError": "無法加載 discovery-alias 設定:{error}", - "ccAliasSaveError": "無法保存 discovery-alias 設定:{error}", + "ccAliasSectionTitle": "Expose in Claude Code (claude/…)", + "ccAliasSectionHint": "Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", + "ccAliasProviderLevelLabel": "Provider default", + "ccAliasModelOverridesLabel": "Per-model overrides", + "ccAliasModelOverrideAriaLabel": "Override for {modelId}", + "ccAliasStateInherit": "Inherit", + "ccAliasStateOn": "On", + "ccAliasStateOff": "Off", + "ccAliasAddModelPlaceholder": "Model id (e.g. gpt-4o)", + "ccAliasAddModelButton": "Add override", + "ccAliasLoadError": "Failed to load discovery-alias settings: {error}", + "ccAliasSaveError": "Failed to save discovery-alias setting: {error}", "compatUpstreamHeadersLabel": "上游額外請求頭", "compatUpstreamHeadersHint": "與修改廠商連線/API 設定同屬高許可權能力,僅可信管理員應使用。這些頭會在 OmniRoute 按廠商 API Key 自動加好鑑權頭之後再合併。若「名稱」與系統已加的頭相同(例如都叫 Authorization),則以你填的值為準,會整段替換自動那條(含 Bearer 權杖),上游請求裡不再使用面板裡儲存的金鑰來生成 Authorization。填錯可能導致 401,請謹慎。每個請求頭單獨一行;部分閘道器需要額外 Authentication 等可在此加。滑鼠移入或聚焦「值」可暫時看明文。點空白處、關閉本面板或切走焦點即儲存。", "compatUpstreamHeaderName": "請求頭名稱", @@ -6194,7 +6209,7 @@ "galadriel": "使用 API 金鑰連線 Galadriel。", "predibase": "$25 美元免費試用額度(30 天有效期)", "chenzk": "OpenAI 相容閘道,在 chenzk.top 提供即時模型目錄。", - "freepik": "使用 Freepik 的 Mystic API 生成圖片。", + "magnific": "使用 Freepik 的 Mystic API 生成圖片。", "freetheai": "免費的 OpenAI 相容閘道,支援透傳模型。", "g4f-gemini": "免費無需金鑰的 g4f.space Gemini 反向代理,每分鐘限制 5 次請求。", "g4f-groq": "免費無需金鑰的 g4f.space Groq 反向代理,每分鐘限制 5 次請求。", @@ -6209,6 +6224,7 @@ "claude": "使用現有的 OAuth 流程連線 Claude Code。", "cline": "使用現有的 OAuth 流程連線 Cline。", "cursor": "使用現有的 OAuth 流程連線 Cursor IDE。", + "cursor-api": "Connect Cursor with a user API key (crsr_...) from cursor.com/dashboard/api; no IDE session needed.", "github": "使用現有的 OAuth 流程連線 GitHub Copilot。", "gitlab-duo": "具有 ai_features + read_user 範圍的 OAuth 應用程式。在此 OmniRoute 實例上設定 GITLAB_DUO_OAUTH_CLIENT_ID 和可選的 GITLAB_DUO_OAUTH_CLIENT_SECRET。", "kilocode": "使用現有的 OAuth 流程連線 Kilo Code。", @@ -6280,18 +6296,6 @@ "codexPoolCoolingDown": "冷卻中", "codexPoolUsed": "已使用", "codexPoolUntil": "截至 {value}", - "ccAliasSectionTitle": "__MISSING__:Expose in Claude Code (claude/…)", - "ccAliasSectionHint": "__MISSING__:Advertise this provider's models under claude/<provider>/<model> mirror ids so Claude Code's gateway model discovery can list them. Off by default — enabling this doubles catalog entries for all clients.", - "ccAliasProviderLevelLabel": "__MISSING__:Provider default", - "ccAliasModelOverridesLabel": "__MISSING__:Per-model overrides", - "ccAliasModelOverrideAriaLabel": "__MISSING__:Override for {modelId}", - "ccAliasStateInherit": "__MISSING__:Inherit", - "ccAliasStateOn": "__MISSING__:On", - "ccAliasStateOff": "__MISSING__:Off", - "ccAliasAddModelPlaceholder": "__MISSING__:Model id (e.g. gpt-4o)", - "ccAliasAddModelButton": "__MISSING__:Add override", - "ccAliasLoadError": "__MISSING__:Failed to load discovery-alias settings: {error}", - "ccAliasSaveError": "__MISSING__:Failed to save discovery-alias setting: {error}", "anonymousFallbackTitle": "匿名後備", "anonymousFallbackDesc": "當所有配置的連接耗盡(配額、積分或到期)時,暫時使用此提供者的無密鑰層級。關閉以跳過此提供者,而不是發送匿名請求 — 當無密鑰層級拒絕它們(401)時建議這樣做。", "anonymousFallbackEnabled": "為 {provider} 啟用匿名後備", @@ -6367,18 +6371,7 @@ "savedModelEndpointSettings": "已儲存的模型端點設定", "searchByModelAria": "按型號搜尋", "selectSupportedEndpoint": "請選擇至少一個受支持的端點", - "antigravityClientProfileHarness": "Harness / CLI", - "autoFetchModels": "自動獲取上游模型", - "autoFetchModelsDisabled": "上游模型自動獲取已禁用", - "autoFetchModelsEnabled": "上游模型自動獲取已啟用", - "autoFetchModelsTooltip": "在需要時獲取並快取上游模型", - "autoFetchModelsToggleFailed": "無法切換上游模型自動獲取", - "autoFetchModelsPartialFailure": "某些連接已更新,但上游模型自動獲取並未在所有地方更改", - "overridesUpstreamModel": "覆蓋上游", - "overridesUpstreamModelHint": "您的設定覆蓋了此上游模型", - "resetToUpstreamDefaults": "恢復上游預設值", - "resetToUpstreamDefaultsSuccess": "已恢復上游模型預設值", - "resetToUpstreamDefaultsFailed": "無法恢復上游模型的預設值" + "antigravityClientProfileHarness": "Harness / CLI" }, "settings": { "title": "設定", @@ -6597,12 +6590,12 @@ "autoDisableDescription": "若提供者連線返回特定的永久封禁訊號(如 HTTP 403\"請驗證您的帳戶\"),則將其永久標記為停用。這會將其從組合輪換中移除。", "autoDisableThreshold": "封禁閾值", "autoDisableThresholdDesc": "觸發永久停用所需的連續封禁訊號次數。", - "autoDisableBannedScope": "__MISSING__:Apply auto-disable to", - "autoDisableBannedScopeDesc": "__MISSING__:Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", - "autoDisableBannedScopeAll": "__MISSING__:All connections", - "autoDisableBannedScopeAllDesc": "__MISSING__:Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", - "autoDisableBannedScopeSubscription": "__MISSING__:Login / subscription accounts only", - "autoDisableBannedScopeSubscriptionDesc": "__MISSING__:Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", + "autoDisableBannedScope": "Apply auto-disable to", + "autoDisableBannedScopeDesc": "Login seats (paid subscriptions and free accounts) can be locked by the upstream if OmniRoute keeps retrying after a ban. Paid prepaid API keys do not have that risk — leave them in the pool so fill-first can use the next key.", + "autoDisableBannedScopeAll": "All connections", + "autoDisableBannedScopeAllDesc": "Deactivate every connection that returns a permanent ban signal, including prepaid API keys.", + "autoDisableBannedScopeSubscription": "Login / subscription accounts only", + "autoDisableBannedScopeSubscriptionDesc": "Deactivate OAuth, cookie, session, and web-login seats (paid or free). Prepaid API keys stay active so routing can fail over.", "customBannedSignals": "禁止關鍵字", "customBannedSignalsDesc": "觸發永久帳戶封鎖偵測的額外關鍵字。內建關鍵字始終適用。", "customBannedSignalsPlaceholder": "例如:api key revoked", @@ -7210,6 +7203,7 @@ "configured": "已設定", "none": "無", "modelOverrideValuePlaceholder": "數值", + "modelOverrideReasoningEffortsPlaceholder": "English comma-separated, e.g. low, medium, high", "addKeyValue": "新增鍵值", "noModelOverrides": "此模型未設定任何覆寫。", "modelOverrideLoadFailed": "載入模型覆寫設定失敗", @@ -8629,6 +8623,27 @@ }, "enableTitle": "啟用引擎", "enableDescription": "在堆疊中最後執行(RTK/Caveman 清理文字後,OmniGlyph 將剩餘部分轉換為圖片),也可透過 omniglyph 模式獨立執行。此為預覽功能,預設為關閉,待端到端驗證完成後才會預設開啟。", + "profileTitle": "Compression profile", + "profileDescription": "The profile is a ceiling, not a floor: a stricter profile cannot be reopened by a per-step override. Measured on this codebase: with coding-safe or balanced, a session that has not accumulated history yet compresses nothing, because both raise the minimum-characters threshold and keep the system prompt, tool schemas and tool results native.", + "profiles": { + "aggressive": { + "label": "Aggressive (default)", + "description": "The policy the published receipts measured. Images the system prompt, tool documentation and dense history." + }, + "balanced": { + "label": "Balanced", + "description": "Keeps live state native and protects the last 8 turns; only collapses older closed history." + }, + "codingSafe": { + "label": "Coding-safe", + "description": "Keeps authority, tool schemas and live tool output native, protecting the last 12 turns. Compresses nothing until the session accumulates history." + }, + "passthrough": { + "label": "Passthrough", + "description": "Routes without transforming. The engine is skipped and the request is forwarded untouched." + } + }, + "profileAria": "Select the OmniGlyph compression profile", "saved": "已儲存。", "saveFailed": "無法儲存。", "enableAria": "啟用 OmniGlyph 引擎", @@ -12506,9 +12521,9 @@ }, "badges": { "radar-supporter": { - "name": "__MISSING__:Radar Supporter", - "description": "__MISSING__:Verified a live OmniRoute Radar supporter feed", - "criteria": "__MISSING__:Verify a signed live Radar supporter feed." + "name": "Radar Supporter", + "description": "Verified a live OmniRoute Radar supporter feed", + "criteria": "Verify a signed live Radar supporter feed." }, "first-token": { "name": "第一個代幣", @@ -13231,7 +13246,7 @@ "localStateSaveFailed": "Failed to save local Radar settings", "guidedCombos": "Guided combos", "offers": "Offers", - "intel": "__MISSING__:Intel" + "intel": "Intel" }, "radarCombosPage": { "title": "Radar guided combos", @@ -13771,36 +13786,36 @@ "trialDays": "{days, plural, one {# day} other {# days}}" }, "radarIntelPage": { - "title": "__MISSING__:Radar Intel", - "subtitle": "__MISSING__:Radar-owned ELO rankings and factual catalog movement.", - "backToRadar": "__MISSING__:Back to Radar", - "loading": "__MISSING__:Loading Intel...", - "refresh": "__MISSING__:Refresh Intel", - "syncing": "__MISSING__:Refreshing...", - "loadFailed": "__MISSING__:We could not refresh Intel. The last verified local cache is kept.", - "empty": "__MISSING__:No verified Intel snapshot is available yet.", - "supporterBadge": "__MISSING__:Radar Supporter", - "methodology": "__MISSING__:Methodology", - "eloMethod": "__MISSING__:ELO, initial {initial}, K={factor}", - "freshness": "__MISSING__:Catalog freshness", - "ageDays": "__MISSING__:{days, plural, one {# day old} other {# days old}}", - "trend": "__MISSING__:Catalog trend", - "modelDelta": "__MISSING__:{current} models, +{added} / -{removed}", - "ranking": "__MISSING__:Model ranking", - "noRankings": "__MISSING__:No confirmed comparisons are available yet.", - "model": "__MISSING__:Model", - "category": "__MISSING__:Category", - "rating": "__MISSING__:Rating", - "matches": "__MISSING__:Matches", + "title": "Radar Intel", + "subtitle": "Radar-owned ELO rankings and factual catalog movement.", + "backToRadar": "Back to Radar", + "loading": "Loading Intel...", + "refresh": "Refresh Intel", + "syncing": "Refreshing...", + "loadFailed": "We could not refresh Intel. The last verified local cache is kept.", + "empty": "No verified Intel snapshot is available yet.", + "supporterBadge": "Radar Supporter", + "methodology": "Methodology", + "eloMethod": "ELO, initial {initial}, K={factor}", + "freshness": "Catalog freshness", + "ageDays": "{days, plural, one {# day old} other {# days old}}", + "trend": "Catalog trend", + "modelDelta": "{current} models, +{added} / -{removed}", + "ranking": "Model ranking", + "noRankings": "No confirmed comparisons are available yet.", + "model": "Model", + "category": "Category", + "rating": "Rating", + "matches": "Matches", "freshnessValues": { - "fresh": "__MISSING__:Fresh", - "aging": "__MISSING__:Aging", - "stale": "__MISSING__:Stale" + "fresh": "Fresh", + "aging": "Aging", + "stale": "Stale" }, "trendValues": { - "growing": "__MISSING__:Growing", - "stable": "__MISSING__:Stable", - "shrinking": "__MISSING__:Shrinking" + "growing": "Growing", + "stable": "Stable", + "shrinking": "Shrinking" } }, "capabilityFilter": { diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index 251ec8cda2..2b1b6269e1 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -505,6 +505,18 @@ function isSchemaAlreadyApplied( // Retroactive guard for 143_radar_local_model_state -> 153. A database // that already created the table must not execute or track it twice. return hasTable(db, "radar_local_model_state"); + case "159": + // Renumbered from 158 (collided with 158_call_logs_error_type on + // release/v3.8.50). Idempotent freepik->magnific slug rewrite: skip + // when provider_connections has no remaining freepik rows (already + // applied under 158, or a DB that never stored Freepik). + if (migration.name !== "rename_freepik_to_magnific") return false; + if (!hasTable(db, "provider_connections")) return false; + return ( + db + .prepare("SELECT 1 FROM provider_connections WHERE provider = 'freepik' LIMIT 1") + .get() == null + ); default: return false; } diff --git a/src/lib/db/migrations/160_rename_freepik_to_magnific.sql b/src/lib/db/migrations/160_rename_freepik_to_magnific.sql new file mode 100644 index 0000000000..3c38e20e39 --- /dev/null +++ b/src/lib/db/migrations/160_rename_freepik_to_magnific.sql @@ -0,0 +1,41 @@ +-- Canonical provider id is now `magnific`. Freepik was the previous slug +-- (Magnific started as Freepik's developer API). Rewrite stored rows so +-- dashboard cards, credentials, and usage stay attached after the rename. +-- `freepik` remains a runtime alias for old URLs and `freepik/` ids. + +UPDATE provider_connections SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE usage_history SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE call_logs SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE registered_keys SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE provider_key_limits SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE quota_snapshots SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE provider_plans SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE hourly_usage_summary SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE daily_usage_summary SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE provider_quota_reset_events SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE session_account_affinity SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE model_context_overrides SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE model_capability_overrides SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE session_model_history SET provider = 'magnific' WHERE provider = 'freepik'; +UPDATE tier_assignments SET provider = 'magnific' WHERE provider = 'freepik'; + +UPDATE usage_history +SET model = 'magnific' || substr(model, 8) +WHERE model LIKE 'freepik/%'; + +UPDATE call_logs +SET model = 'magnific' || substr(model, 8) +WHERE model LIKE 'freepik/%'; + +UPDATE hourly_usage_summary +SET model = 'magnific' || substr(model, 8) +WHERE model LIKE 'freepik/%'; + +UPDATE daily_usage_summary +SET model = 'magnific' || substr(model, 8) +WHERE model LIKE 'freepik/%'; + +UPDATE key_value +SET key = 'magnific' +WHERE namespace IN ('cliToolLastConfig', 'cliToolInitialConfig') + AND key = 'freepik'; diff --git a/src/lib/modelMetadataRegistry.ts b/src/lib/modelMetadataRegistry.ts index c5c0bbbb7c..5e477084ea 100644 --- a/src/lib/modelMetadataRegistry.ts +++ b/src/lib/modelMetadataRegistry.ts @@ -29,7 +29,6 @@ import { CANONICAL_EFFORT_VALUES, extendCodexGpt56EffortValues, } from "@/shared/reasoning/effortStandardization"; -import type { ModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot"; const MODEL_METADATA_SCHEMA_VERSION = "model-metadata-v1"; diff --git a/src/lib/providers/catalog.ts b/src/lib/providers/catalog.ts index 1f53542c6b..0491b4d899 100644 --- a/src/lib/providers/catalog.ts +++ b/src/lib/providers/catalog.ts @@ -9,6 +9,7 @@ import { UPSTREAM_PROXY_PROVIDERS, WEB_COOKIE_PROVIDERS, isClaudeCodeCompatibleProvider, + resolveProviderId, supportsApiKeyOnFreeProvider, supportsDualAuthProvider, type RiskNoticeVariant, @@ -194,9 +195,10 @@ export function getStaticProviderCatalogGroup( export function resolveStaticProviderCatalogEntry( providerId: string ): ResolvedStaticProviderCatalogEntry | null { + const canonicalId = resolveProviderId(providerId); for (const category of STATIC_PROVIDER_CATALOG_RESOLUTION_ORDER) { const group = STATIC_PROVIDER_CATALOG_GROUPS[category]; - const provider = group.providers[providerId]; + const provider = group.providers[canonicalId] ?? group.providers[providerId]; if (!provider) continue; return { ...provider, diff --git a/src/lib/providers/imageValidation.ts b/src/lib/providers/imageValidation.ts index 444517a608..1d91eb18ee 100644 --- a/src/lib/providers/imageValidation.ts +++ b/src/lib/providers/imageValidation.ts @@ -28,6 +28,11 @@ const IMAGE_PROVIDER_VALIDATION_ENDPOINTS: Record< topaz: { path: "/account/v1/credits/balance", }, + magnific: { + // GET /v1/ai/mystic lists tasks and does not start a paid generation. + baseUrl: "https://api.magnific.com", + path: "/v1/ai/mystic", + }, }; function normalizeBaseUrl(baseUrl: string) { @@ -86,9 +91,15 @@ function buildImageProviderValidationHeaders( break; case "none": break; - default: - headers.Authorization = `Bearer ${apiKey}`; + default: { + const headerName = String(imageProvider?.authHeader || "").trim(); + if (headerName.toLowerCase().startsWith("x-")) { + headers[headerName] = apiKey; + } else { + headers.Authorization = `Bearer ${apiKey}`; + } break; + } } } @@ -109,7 +120,9 @@ export async function validateImageProviderApiKey({ providerSpecificData = {}, }: any) { const imageProvider = getImageProvider(provider); - const validationConfig = IMAGE_PROVIDER_VALIDATION_ENDPOINTS[provider]; + const validationConfig = + IMAGE_PROVIDER_VALIDATION_ENDPOINTS[imageProvider?.id] || + IMAGE_PROVIDER_VALIDATION_ENDPOINTS[provider]; if (!imageProvider || !validationConfig) { return { valid: false, error: "Provider validation not supported", unsupported: true }; diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index df29569f8b..bb970b7af5 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -141,6 +141,7 @@ export { validateWebCookieProvider, bytezValidationResultFromStatus }; // They are re-exported above to preserve the historical public surface. export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) { + provider = typeof provider === "string" ? resolveProviderId(provider) : provider; const requiresApiKey = !providerAllowsOptionalApiKey(provider); const isLocal = isLocalProvider(provider); @@ -214,6 +215,8 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi validateImageProviderApiKey({ provider: "recraft", apiKey, providerSpecificData }), topaz: ({ apiKey, providerSpecificData }: any) => validateImageProviderApiKey({ provider: "topaz", apiKey, providerSpecificData }), + magnific: ({ apiKey, providerSpecificData }: any) => + validateImageProviderApiKey({ provider: "magnific", apiKey, providerSpecificData }), elevenlabs: validateElevenLabsProvider, inworld: validateInworldProvider, kie: validateKieProvider, diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index d5754eeea4..23f9c53b39 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1,21 +1,12 @@ // Re-export service kinds from leaf module (avoids circular dep with providerSchema) export type { ServiceKind } from "./serviceKinds"; -export { SERVICE_KIND_VALUES } from "./serviceKinds"; - export type RiskNoticeVariant = "oauth" | "webCookie" | "deprecated" | "embedded-service"; -export interface ProviderRiskNoticeFields { - subscriptionRisk?: boolean; - riskNoticeVariant?: RiskNoticeVariant; - isEmbeddedService?: boolean; -} - import { NOAUTH_PROVIDERS } from "./providers/noauth"; export { supportsNoAuthProviderProxy } from "./providers/noauth"; import { OAUTH_PROVIDERS } from "./providers/oauth"; import { WEB_COOKIE_PROVIDERS, resolveWebProviderHost } from "./providers/web-cookie"; export { resolveWebProviderHost }; -export type { WebProviderHostLink } from "./providers/web-cookie"; import { APIKEY_PROVIDERS } from "./providers/apikey"; import { LOCAL_PROVIDERS } from "./providers/local"; import { SEARCH_PROVIDERS } from "./providers/search"; @@ -70,6 +61,10 @@ export const PROVIDER_CONNECTION_FAMILY_ALIASES: Readonly, { }, }); -export type AiProviderId = - | keyof typeof NOAUTH_PROVIDERS - | keyof typeof OAUTH_PROVIDERS - | keyof typeof APIKEY_PROVIDERS - | keyof typeof WEB_COOKIE_PROVIDERS - | keyof typeof LOCAL_PROVIDERS - | keyof typeof SEARCH_PROVIDERS - | keyof typeof AUDIO_ONLY_PROVIDERS - | keyof typeof UPSTREAM_PROXY_PROVIDERS - | keyof typeof CLOUD_AGENT_PROVIDERS - | keyof typeof SYSTEM_PROVIDERS; - export type AiProviderDefinition = | (typeof NOAUTH_PROVIDERS)[keyof typeof NOAUTH_PROVIDERS] | (typeof OAUTH_PROVIDERS)[keyof typeof OAUTH_PROVIDERS] diff --git a/src/shared/constants/providers/apikey/specialty-media.ts b/src/shared/constants/providers/apikey/specialty-media.ts index e66e97bc2b..b6924d239e 100644 --- a/src/shared/constants/providers/apikey/specialty-media.ts +++ b/src/shared/constants/providers/apikey/specialty-media.ts @@ -85,15 +85,16 @@ export const APIKEY_PROVIDERS_SPECIALTY = { website: "https://ideogram.ai", authHint: "Get API key at ideogram.ai/docs/api", }, - freepik: { - id: "freepik", - alias: "fpk", - name: "Freepik (Mystic)", + magnific: { + id: "magnific", + alias: "freepik", + name: "Magnific", icon: "image", color: "#1B9E7F", - textIcon: "FP", - website: "https://freepik.com", - authHint: "Get API key at freepik.com/developers (Mystic image endpoint)", + textIcon: "MG", + website: "https://www.magnific.com", + authHint: + "Get an API key at magnific.com/user/api-keys (header x-magnific-api-key). Legacy Freepik developer keys still work.", hasFree: true, freeNote: "One-time ~€5 API credit for new accounts; pay-per-use afterward.", }, diff --git a/src/shared/validation/compressionConfigSchemas.ts b/src/shared/validation/compressionConfigSchemas.ts index 50a21aaf10..e0ba0b6fe5 100644 --- a/src/shared/validation/compressionConfigSchemas.ts +++ b/src/shared/validation/compressionConfigSchemas.ts @@ -71,6 +71,8 @@ export const rtkConfigSchema = z trustProjectFilters: z.boolean().optional(), rawOutputRetention: rtkRawOutputRetentionSchema.optional(), rawOutputMaxBytes: z.number().int().min(1024).max(10_000_000).optional(), + rawOutputMaxFiles: z.number().int().min(1).max(10_000_000).optional(), + rawOutputMaxAgeDays: z.number().int().min(1).max(3650).optional(), enableGrouping: z.boolean().optional(), groupingThreshold: z.number().int().min(2).max(100).optional(), stripCodeComments: z.boolean().optional(), diff --git a/stryker.conf.json b/stryker.conf.json index 08c35843f5..42d5eb7412 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -42,6 +42,7 @@ "plugins": ["@stryker-mutator/tap-runner"], "tap": { "testFiles": [ + "tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts", "tests/unit/7993-noauth-proxy-routing.test.ts", "tests/unit/8200-perplexity-web-401-cooldown.test.ts", "tests/unit/8247-accountfallback-model-unhealthy.test.ts", @@ -51,7 +52,6 @@ "tests/unit/8396-cooldown-429-cap.test.ts", "tests/unit/8488-capability-filter-fail-closed.test.ts", "tests/unit/8779-agy-prefix-credential-lookup.test.ts", - "tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts", "tests/unit/account-fallback-anthropic-quota.test.ts", "tests/unit/account-fallback-cf1010-no-retry-8775.test.ts", "tests/unit/account-fallback-lockout-eviction.test.ts", @@ -64,12 +64,12 @@ "tests/unit/adobe-firefly.test.ts", "tests/unit/agentrouter-error-rules.test.ts", "tests/unit/agentrouter-lock-scope-10334.test.ts", + "tests/unit/aihorde-optional-api-key.test.ts", "tests/unit/alibaba-free-tier-exhaustion.test.ts", "tests/unit/anthropic-thinking-signature-recovery.test.ts", "tests/unit/antigravity-429-quota-tdd.test.ts", "tests/unit/antigravity-prefer-stored-project.test.ts", "tests/unit/api-key-policy-noauth-allowed-connections.test.ts", - "tests/unit/api-key-policy-noauth-allowed-connections.test.ts", "tests/unit/api-key-rotator-health.test.ts", "tests/unit/api/jobs.test.ts", "tests/unit/appearance-widget-settings-schema.test.ts", @@ -109,9 +109,9 @@ "tests/unit/chatcore-codex-account-pool.test.ts", "tests/unit/chatcore-compression-integration.test.ts", "tests/unit/chatcore-executor-helpers.test.ts", - "tests/unit/chatcore-header-drop-warn-dedupe-10315.test.ts", "tests/unit/chatcore-executor-proxy.test.ts", "tests/unit/chatcore-extracted-modules-3821.test.ts", + "tests/unit/chatcore-header-drop-warn-dedupe-10315.test.ts", "tests/unit/chatcore-headers.test.ts", "tests/unit/chatcore-imports-cleanly.test.ts", "tests/unit/chatcore-log-truncation.test.ts", @@ -145,13 +145,13 @@ "tests/unit/clinepass-provider.test.ts", "tests/unit/cliproxyapi-dedicated-credential-7645.test.ts", "tests/unit/cliproxyapi-model-mapping-dispatch.test.ts", - "tests/unit/cliproxyapi-model-mapping-dispatch.test.ts", - "tests/unit/cliproxyapi-model-mapping-dispatch.test.ts", "tests/unit/codex-failover.test.ts", "tests/unit/codex-quota-selection-hydration.test.ts", "tests/unit/codex-responses-to-chat-9161.test.ts", + "tests/unit/codex-same-account-transport-retry-9708.test.ts", "tests/unit/codex-session-affinity-reset-aware-5903.test.ts", "tests/unit/codex-stream-false.test.ts", + "tests/unit/codex-turn-state.test.ts", "tests/unit/collect-metrics-module-coverage.test.ts", "tests/unit/combo-499-abort.test.ts", "tests/unit/combo-account-allowlist-3266.test.ts", @@ -217,21 +217,24 @@ "tests/unit/cursor-renewal.test.ts", "tests/unit/custom-model-target-format.test.ts", "tests/unit/db-reset-module-state.test.ts", + "tests/unit/db/stats-dbstat-optional.test.ts", "tests/unit/ddg-circuit-breaker-null-content-6999-7000.test.ts", "tests/unit/domain-persistence.test.ts", "tests/unit/edgetts-provider.test.ts", + "tests/unit/embedding-account-cooldown-10347.test.ts", + "tests/unit/embedding-cooldown-integration-10347.test.ts", "tests/unit/embeddings-auth.test.ts", "tests/unit/error-classification.test.ts", - "tests/unit/executor-contract-violation-terminal.test.ts", "tests/unit/error-message-sanitization.test.ts", "tests/unit/error-sensitive-redaction.test.ts", "tests/unit/execute-chat-resource-pressure-breaker.test.ts", "tests/unit/executor-antigravity.test.ts", + "tests/unit/executor-contract-violation-terminal.test.ts", "tests/unit/executor-devin-cli-agentic-acp.test.ts", "tests/unit/executor-web-cookie-sweep.test.ts", "tests/unit/format-provider-error-cause.test.ts", "tests/unit/forwarded-header-budget.test.ts", - "tests/unit/gemini-web-capabilities-9356.test.ts", + "tests/unit/fusion-vision-panel-3378.test.ts", "tests/unit/gemini-web-capabilities-9356.test.ts", "tests/unit/gemini-web-missing-browser-3516.test.ts", "tests/unit/grok-cli-oauth.test.ts", @@ -281,6 +284,9 @@ "tests/unit/plan3-p0.test.ts", "tests/unit/plugin-sandbox-permissions.test.ts", "tests/unit/plugins-route-error-sanitization.test.ts", + "tests/unit/probe-gate-autodisable.test.ts", + "tests/unit/probe-production-path.test.ts", + "tests/unit/probe-testall-isolation.test.ts", "tests/unit/provider-breaker-halfopen-recovery.test.ts", "tests/unit/provider-error-rules.test.ts", "tests/unit/provider-health-matrix.test.ts", @@ -302,11 +308,11 @@ "tests/unit/rate-limit-queue-timeout-lockout.test.ts", "tests/unit/repro-7503-no-choices.test.ts", "tests/unit/repro-9486.test.ts", - "tests/unit/repro-9486.test.ts", "tests/unit/repro-9630-combo-false-503.test.ts", "tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts", "tests/unit/resilience-connections.test.ts", "tests/unit/responses-handler.test.ts", + "tests/unit/responses-passthrough-openai-compatible.test.ts", "tests/unit/rotation-config-omniroute.test.ts", "tests/unit/route-explainability.test.ts", "tests/unit/route-guard-acp-agents-local-only.test.ts", @@ -320,6 +326,7 @@ "tests/unit/route-guard-provider-login-local-only.test.ts", "tests/unit/route-guard-qwen-settings-local-only.test.ts", "tests/unit/router-strategies.test.ts", + "tests/unit/routing-adaptive-e2e.test.ts", "tests/unit/rule12-error-sanitization-sweep.test.ts", "tests/unit/serial/combo-health-autopilot.test.ts", "tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts", @@ -335,9 +342,9 @@ "tests/unit/skip-provider-breaker-consumer-2743.test.ts", "tests/unit/sse-auth-antigravity-credits.test.ts", "tests/unit/sse-auth-codex-account-pool.test.ts", + "tests/unit/sse-auth-exclusive-leases.test.ts", "tests/unit/sse-auth-resource-404.test.ts", "tests/unit/sse-auth.test.ts", - "tests/unit/db/stats-dbstat-optional.test.ts", "tests/unit/stream-early-eof-breaker.test.ts", "tests/unit/stream-readiness.test.ts", "tests/unit/strict-random-deck.test.ts", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 0afdd55ac3..4fc9db6fdd 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -1534,6 +1534,38 @@ "stream": "https://api2.cursor.sh" } }, + "cursor-api": { + "format": "cursor", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/connect+proto", + "User-Agent": "Cursor/3.9", + "connect-accept-encoding": "gzip", + "connect-protocol-version": "1" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/connect+proto", + "User-Agent": "Cursor/3.9", + "connect-accept-encoding": "gzip", + "connect-protocol-version": "1" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/connect+proto", + "User-Agent": "Cursor/3.9", + "connect-accept-encoding": "gzip", + "connect-protocol-version": "1" + } + }, + "url": { + "nonStream": "https://api2.cursor.sh", + "stream": "https://api2.cursor.sh" + } + }, "dahl": { "format": "openai", "headers": { @@ -3740,6 +3772,52 @@ "stream": "https://models.mixlayer.ai/v1/chat/completions" } }, + "mlx-gemma": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "http://localhost:11435/v1", + "stream": "http://localhost:11435/v1" + } + }, + "mlx-qwen": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "http://localhost:11436/v1", + "stream": "http://localhost:11436/v1" + } + }, "mnn-ai": { "format": "openai", "headers": { @@ -5509,6 +5587,29 @@ "stream": "https://api.together.xyz/v1/chat/completions" } }, + "token-kiosk": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://agent-router.gaib.ai/v1/chat/completions", + "stream": "https://agent-router.gaib.ai/v1/chat/completions" + } + }, "tokenreply": { "format": "openai", "headers": { diff --git a/tests/unit/alibaba-image-media.test.ts b/tests/unit/alibaba-image-media.test.ts index 539df1437e..4a9e3e0a23 100644 --- a/tests/unit/alibaba-image-media.test.ts +++ b/tests/unit/alibaba-image-media.test.ts @@ -49,7 +49,7 @@ test("Alibaba registration preserves existing bare duplicate-model routing", () model: "z-image-turbo", }); assert.deepEqual(parseImageModel("qwen-image-2.0"), { - provider: "lmarena", + provider: "bailian-coding-plan", model: "qwen-image-2.0", }); assert.deepEqual(parseImageModel("qwen-image-3.0-pro"), { diff --git a/tests/unit/antigravity-retired-public-models.test.ts b/tests/unit/antigravity-retired-public-models.test.ts index 5f778d5965..890509bb73 100644 --- a/tests/unit/antigravity-retired-public-models.test.ts +++ b/tests/unit/antigravity-retired-public-models.test.ts @@ -34,6 +34,7 @@ const EXPECTED_LEADING_MODEL_ORDER = [ "gemini-3.7-flash-high", "gemini-3.7-flash-medium", "gemini-3.7-flash-low", + "gemini-3.7-flash-tiered", "gemini-pro-agent", "gemini-3.1-pro-low", "gemini-3.1-flash-lite", diff --git a/tests/unit/freepik-image-handler.test.ts b/tests/unit/freepik-image-handler.test.ts deleted file mode 100644 index 610445e5af..0000000000 --- a/tests/unit/freepik-image-handler.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import dns from "node:dns"; - -import { handleImageGeneration } from "../../open-sse/handlers/imageGeneration.ts"; -import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts"; -import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers.ts"; -import { IMAGE_ONLY_PROVIDER_IDS } from "../../src/shared/constants/providers.ts"; - -// Stub DNS for fetchRemoteImage/direct-fetch DNS-rebinding guards, mirroring -// tests/unit/nanobanana-image-handler.test.ts. -const originalDnsLookup = dns.promises.lookup; -(dns.promises as { lookup: unknown }).lookup = (async ( - _hostname: string, - options?: { all?: boolean } -) => { - const record = { address: "203.0.113.1", family: 4 }; - return options && options.all ? [record] : record; -}) as typeof dns.promises.lookup; -process.on("exit", () => { - (dns.promises as { lookup: unknown }).lookup = originalDnsLookup; -}); - -test("freepik provider is registered (registry shape)", () => { - assert.ok(APIKEY_PROVIDERS.freepik, "freepik should be in APIKEY_PROVIDERS"); - assert.equal(APIKEY_PROVIDERS.freepik.id, "freepik"); - assert.ok(IMAGE_ONLY_PROVIDER_IDS.has("freepik"), "freepik should be in IMAGE_ONLY_PROVIDER_IDS"); - - const provider = IMAGE_PROVIDERS.freepik; - assert.ok(provider, "freepik should be in IMAGE_PROVIDERS"); - assert.equal(provider.format, "freepik-image"); - assert.equal(provider.authType, "apikey"); - assert.equal(provider.authHeader, "x-freepik-api-key"); - assert.ok(provider.models.some((m) => m.id === "realism")); - assert.ok(provider.models.some((m) => m.id === "fluid")); -}); - -test("handleImageGeneration(freepik): async submit+poll returns b64_json payload", async () => { - const originalFetch = globalThis.fetch; - let pollCount = 0; - - globalThis.fetch = (async (url: string, options: { headers?: Record; body?: string } = {}) => { - const u = String(url); - - if (u === "https://api.freepik.com/v1/ai/mystic") { - assert.equal(options.headers?.["x-freepik-api-key"], "test-key"); - const parsed = JSON.parse(options.body as string); - assert.equal(parsed.prompt, "a red panda astronaut"); - assert.equal(parsed.model, "realism"); - return new Response( - JSON.stringify({ data: { task_id: "task-freepik-1", status: "CREATED" } }), - { status: 200, headers: { "content-type": "application/json" } } - ); - } - - if (u === "https://api.freepik.com/v1/ai/mystic/task-freepik-1") { - pollCount += 1; - if (pollCount < 2) { - return new Response( - JSON.stringify({ data: { task_id: "task-freepik-1", status: "IN_PROGRESS" } }), - { status: 200, headers: { "content-type": "application/json" } } - ); - } - return new Response( - JSON.stringify({ - data: { - task_id: "task-freepik-1", - status: "COMPLETED", - generated: ["https://cdn.example.com/freepik-result.png"], - }, - }), - { status: 200, headers: { "content-type": "application/json" } } - ); - } - - if (u === "https://cdn.example.com/freepik-result.png") { - return new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), { status: 200 }); - } - - throw new Error(`Unexpected URL: ${u}`); - }) as typeof fetch; - - try { - const result = await handleImageGeneration({ - body: { - model: "freepik/realism", - prompt: "a red panda astronaut", - poll_interval_ms: 1, - }, - credentials: { apiKey: "test-key" }, - log: null, - }); - - assert.equal(result.success, true); - assert.equal(result.data.data.length, 1); - assert.equal(result.data.data[0].b64_json, "iVBORw=="); - assert.equal(pollCount, 2); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("handleImageGeneration(freepik): FAILED status returns sanitized 502 error", async () => { - const originalFetch = globalThis.fetch; - - globalThis.fetch = (async (url: string) => { - const u = String(url); - if (u === "https://api.freepik.com/v1/ai/mystic") { - return new Response(JSON.stringify({ data: { task_id: "task-fail", status: "CREATED" } }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - if (u === "https://api.freepik.com/v1/ai/mystic/task-fail") { - return new Response(JSON.stringify({ data: { task_id: "task-fail", status: "FAILED" } }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - throw new Error(`Unexpected URL: ${u}`); - }) as typeof fetch; - - try { - const result = await handleImageGeneration({ - body: { model: "freepik/realism", prompt: "broken prompt", poll_interval_ms: 1 }, - credentials: { apiKey: "test-key" }, - log: null, - }); - - assert.equal(result.success, false); - assert.equal(result.status, 502); - assert.match(result.error, /Freepik Mystic image generation failed/); - // Hard Rule #12: error responses must never leak a raw stack trace / file path. - assert.ok(!result.error.includes("at /")); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("handleImageGeneration(freepik): submit error response is sanitized, not raw upstream body", async () => { - const originalFetch = globalThis.fetch; - - globalThis.fetch = (async () => { - // Simulate an upstream error body containing something that looks like a - // stack trace / absolute source path, to prove sanitizeErrorMessage runs. - const stackyBody = "Error: boom\n at /srv/app/handlers/mystic.ts:42:10"; - return new Response(stackyBody, { status: 500 }); - }) as typeof fetch; - - try { - const result = await handleImageGeneration({ - body: { model: "freepik/realism", prompt: "x" }, - credentials: { apiKey: "test-key" }, - log: null, - }); - - assert.equal(result.success, false); - assert.equal(result.status, 500); - assert.ok(!result.error.includes("/srv/app/handlers/mystic.ts")); - } finally { - globalThis.fetch = originalFetch; - } -}); diff --git a/tests/unit/hard-session-lease-bypass-inventory.test.ts b/tests/unit/hard-session-lease-bypass-inventory.test.ts index 428b75bdcf..9736a33e07 100644 --- a/tests/unit/hard-session-lease-bypass-inventory.test.ts +++ b/tests/unit/hard-session-lease-bypass-inventory.test.ts @@ -13,15 +13,19 @@ type BypassClass = "A" | "B" | "C"; const EXPECTED: Record> = { credential: { - "open-sse/handlers/chatCore.ts": 1, + "open-sse/handlers/chatCore.ts": 2, "open-sse/services/imageCombo.ts": 1, + "open-sse/services/speechCombo.ts": 1, + "open-sse/services/videoCombo.ts": 2, "src/app/api/compression/compare/verify/route.ts": 1, "src/app/api/internal/codex-responses-ws/route.ts": 1, "src/app/api/search/providers/route.ts": 3, "src/app/api/v1/audio/speech/route.ts": 1, - "src/app/api/v1/audio/transcriptions/route.ts": 1, + "src/app/api/v1/_shared/videoModelResolution.ts": 1, + "src/app/api/v1/audio/transcriptions/route.ts": 2, "src/app/api/v1/audio/translations/route.ts": 1, - "src/app/api/v1/images/edits/route.ts": 5, + "src/app/api/v1/classify/route.ts": 1, + "src/app/api/v1/images/edits/route.ts": 6, "src/app/api/v1/images/generations/route.ts": 3, "src/app/api/v1/images/upscale/route.ts": 1, "src/app/api/v1/messages/count_tokens/route.ts": 1, @@ -32,8 +36,9 @@ const EXPECTED: Record> = { "src/app/api/v1/providers/[provider]/images/generations/route.ts": 1, "src/app/api/v1/rerank/route.ts": 2, "src/app/api/v1/search/route.ts": 2, + "src/app/api/v1/segment/route.ts": 1, "src/app/api/v1/session-leases/route.ts": 1, - "src/app/api/v1/videos/generations/route.ts": 3, + "src/app/api/v1/videos/generations/route.ts": 2, "src/app/api/v1/web/fetch/route.ts": 1, "src/lib/embeddings/service.ts": 2, "src/lib/memory/embedding/index.ts": 1, @@ -49,6 +54,7 @@ const EXPECTED: Record> = { "open-sse/handlers/chatCore/cliproxyapiCredentials.ts": 1, "open-sse/handlers/imageGeneration.ts": 1, "open-sse/handlers/imageGeneration/providers/chatgptWeb.ts": 1, + "open-sse/handlers/imageGeneration/providers/geminiWeb.ts": 1, "open-sse/handlers/videoGeneration.ts": 1, "open-sse/services/compression/eval/executorModelClient.ts": 1, "src/lib/compression/judgeModelClient.ts": 1, @@ -57,6 +63,7 @@ const EXPECTED: Record> = { connection: { "open-sse/handlers/autoComboCandidates.ts": 1, "open-sse/handlers/chatCore.ts": 2, + "open-sse/handlers/cursorCliProxy.ts": 1, "open-sse/services/alibabaFreeTier.ts": 1, "open-sse/services/alibabaFreeTierQuotaFetcher.ts": 1, "open-sse/services/combo/providerWildcard.ts": 1, @@ -149,6 +156,7 @@ const CLASSIFICATION: Record> = { "open-sse/handlers/chatCore/cliproxyapiCredentials.ts": "A", "open-sse/handlers/imageGeneration.ts": "B", "open-sse/handlers/imageGeneration/providers/chatgptWeb.ts": "B", + "open-sse/handlers/imageGeneration/providers/geminiWeb.ts": "B", "open-sse/handlers/videoGeneration.ts": "B", "open-sse/services/compression/eval/executorModelClient.ts": "B", "src/lib/compression/judgeModelClient.ts": "B", diff --git a/tests/unit/instrumentation-warm-catalog-cache.test.ts b/tests/unit/instrumentation-warm-catalog-cache.test.ts index 5f73283249..84fa161e78 100644 --- a/tests/unit/instrumentation-warm-catalog-cache.test.ts +++ b/tests/unit/instrumentation-warm-catalog-cache.test.ts @@ -70,10 +70,15 @@ test.after(async () => { const REAL_FETCH = globalThis.fetch; let fetchCallCount = 0; +function isOpenRouterCatalogUrl(input: RequestInfo | URL): boolean { + const url = String(input instanceof Request ? input.url : input); + return url.includes("openrouter.ai"); +} + function installFakeOpenRouterFetch(): void { fetchCallCount = 0; - globalThis.fetch = (async () => { - fetchCallCount++; + globalThis.fetch = (async (input: RequestInfo | URL) => { + if (isOpenRouterCatalogUrl(input)) fetchCallCount++; return new Response(JSON.stringify({ data: [{ id: "test/fake-model", architecture: {} }] }), { status: 200, headers: { "content-type": "application/json" }, @@ -83,8 +88,8 @@ function installFakeOpenRouterFetch(): void { function installFailingOpenRouterFetch(): void { fetchCallCount = 0; - globalThis.fetch = (async () => { - fetchCallCount++; + globalThis.fetch = (async (input: RequestInfo | URL) => { + if (isOpenRouterCatalogUrl(input)) fetchCallCount++; throw new Error("simulated OpenRouter network failure"); }) as typeof fetch; } diff --git a/tests/unit/magnific-image-handler.test.ts b/tests/unit/magnific-image-handler.test.ts new file mode 100644 index 0000000000..5b2c934921 --- /dev/null +++ b/tests/unit/magnific-image-handler.test.ts @@ -0,0 +1,233 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import dns from "node:dns"; + +import { handleImageGeneration } from "../../open-sse/handlers/imageGeneration.ts"; +import { + IMAGE_PROVIDERS, + getImageProvider, + parseImageModel, +} from "../../open-sse/config/imageRegistry.ts"; +import { APIKEY_PROVIDERS, resolveProviderId } from "../../src/shared/constants/providers.ts"; +import { IMAGE_ONLY_PROVIDER_IDS } from "../../src/shared/constants/providers.ts"; +import { connectionBelongsToProviderPage } from "../../src/app/(dashboard)/dashboard/providers/providerPageUtils.ts"; + +// Stub DNS for fetchRemoteImage/direct-fetch DNS-rebinding guards, mirroring +// tests/unit/nanobanana-image-handler.test.ts. +const originalDnsLookup = dns.promises.lookup; +(dns.promises as { lookup: unknown }).lookup = (async ( + _hostname: string, + options?: { all?: boolean } +) => { + const record = { address: "203.0.113.1", family: 4 }; + return options && options.all ? [record] : record; +}) as typeof dns.promises.lookup; +process.on("exit", () => { + (dns.promises as { lookup: unknown }).lookup = originalDnsLookup; +}); + +test("magnific provider is registered (registry shape)", () => { + assert.ok(APIKEY_PROVIDERS.magnific, "magnific should be in APIKEY_PROVIDERS"); + assert.equal(APIKEY_PROVIDERS.magnific.id, "magnific"); + assert.ok( + IMAGE_ONLY_PROVIDER_IDS.has("magnific"), + "magnific should be in IMAGE_ONLY_PROVIDER_IDS" + ); + assert.ok(!IMAGE_ONLY_PROVIDER_IDS.has("freepik")); + + const provider = IMAGE_PROVIDERS.magnific; + assert.ok(provider, "magnific should be in IMAGE_PROVIDERS"); + assert.equal(provider.format, "magnific-image"); + assert.equal(provider.authType, "apikey"); + assert.equal(provider.authHeader, "x-magnific-api-key"); + assert.equal(provider.baseUrl, "https://api.magnific.com/v1/ai/mystic"); + assert.equal(provider.alias, "freepik"); + assert.ok(provider.models.some((m) => m.id === "realism")); + assert.ok(provider.models.some((m) => m.id === "fluid")); + assert.equal(APIKEY_PROVIDERS.magnific.alias, "freepik"); + assert.equal(resolveProviderId("freepik"), "magnific"); + assert.equal(resolveProviderId("magnific"), "magnific"); + assert.equal(getImageProvider("freepik")?.id, "magnific"); + assert.deepEqual(parseImageModel("freepik/realism"), { provider: "magnific", model: "realism" }); + assert.deepEqual(parseImageModel("magnific/realism"), { provider: "magnific", model: "realism" }); + assert.equal(connectionBelongsToProviderPage("freepik", "magnific"), true); + assert.equal(connectionBelongsToProviderPage("magnific", "freepik"), true); +}); + +test("handleImageGeneration(magnific): async submit+poll returns b64_json payload", async () => { + const originalFetch = globalThis.fetch; + let pollCount = 0; + + globalThis.fetch = (async ( + url: string, + options: { headers?: Record; body?: string } = {} + ) => { + const u = String(url); + + if (u === "https://api.magnific.com/v1/ai/mystic") { + assert.equal(options.headers?.["x-magnific-api-key"], "test-key"); + const parsed = JSON.parse(options.body as string); + assert.equal(parsed.prompt, "a red panda astronaut"); + assert.equal(parsed.model, "realism"); + return new Response( + JSON.stringify({ data: { task_id: "task-magnific-1", status: "CREATED" } }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + + if (u === "https://api.magnific.com/v1/ai/mystic/task-magnific-1") { + pollCount += 1; + if (pollCount < 2) { + return new Response( + JSON.stringify({ data: { task_id: "task-magnific-1", status: "IN_PROGRESS" } }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + return new Response( + JSON.stringify({ + data: { + task_id: "task-magnific-1", + status: "COMPLETED", + generated: ["https://cdn.example.com/magnific-result.png"], + }, + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + + if (u === "https://cdn.example.com/magnific-result.png") { + return new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), { status: 200 }); + } + + throw new Error(`Unexpected URL: ${u}`); + }) as typeof fetch; + + try { + const result = await handleImageGeneration({ + body: { + model: "magnific/realism", + prompt: "a red panda astronaut", + poll_interval_ms: 1, + }, + credentials: { apiKey: "test-key" }, + log: null, + }); + + assert.equal(result.success, true); + assert.equal(result.data.data.length, 1); + assert.equal(result.data.data[0].b64_json, "iVBORw=="); + assert.equal(pollCount, 2); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("handleImageGeneration(magnific): FAILED status returns sanitized 502 error", async () => { + const originalFetch = globalThis.fetch; + + globalThis.fetch = (async (url: string) => { + const u = String(url); + if (u === "https://api.magnific.com/v1/ai/mystic") { + return new Response(JSON.stringify({ data: { task_id: "task-fail", status: "CREATED" } }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (u === "https://api.magnific.com/v1/ai/mystic/task-fail") { + return new Response(JSON.stringify({ data: { task_id: "task-fail", status: "FAILED" } }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`Unexpected URL: ${u}`); + }) as typeof fetch; + + try { + const result = await handleImageGeneration({ + body: { model: "magnific/realism", prompt: "broken prompt", poll_interval_ms: 1 }, + credentials: { apiKey: "test-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 502); + assert.match(result.error, /Magnific Mystic image generation failed/); + // Hard Rule #12: error responses must never leak a raw stack trace / file path. + assert.ok(!result.error.includes("at /")); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("handleImageGeneration(magnific): submit error response is sanitized, not raw upstream body", async () => { + const originalFetch = globalThis.fetch; + + globalThis.fetch = (async () => { + // Simulate an upstream error body containing something that looks like a + // stack trace / absolute source path, to prove sanitizeErrorMessage runs. + const stackyBody = "Error: boom\n at /srv/app/handlers/mystic.ts:42:10"; + return new Response(stackyBody, { status: 500 }); + }) as typeof fetch; + + try { + const result = await handleImageGeneration({ + body: { model: "magnific/realism", prompt: "x" }, + credentials: { apiKey: "test-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 500); + assert.ok(!result.error.includes("/srv/app/handlers/mystic.ts")); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("handleImageGeneration(freepik/realism): legacy alias routes to the Magnific Mystic adapter", async () => { + const originalFetch = globalThis.fetch; + let sawSubmit = false; + + globalThis.fetch = (async ( + url: string, + options: { headers?: Record; body?: string } = {} + ) => { + const u = String(url); + if (u === "https://api.magnific.com/v1/ai/mystic") { + sawSubmit = true; + assert.equal(options.headers?.["x-magnific-api-key"], "test-key"); + return new Response(JSON.stringify({ data: { task_id: "task-alias", status: "CREATED" } }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (u === "https://api.magnific.com/v1/ai/mystic/task-alias") { + return new Response( + JSON.stringify({ + data: { + task_id: "task-alias", + status: "COMPLETED", + generated: ["https://cdn.example.com/magnific-result.png"], + }, + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + if (u === "https://cdn.example.com/magnific-result.png") { + return new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), { status: 200 }); + } + throw new Error(`Unexpected URL: ${u}`); + }) as typeof fetch; + + try { + const result = await handleImageGeneration({ + body: { model: "freepik/realism", prompt: "alias probe", poll_interval_ms: 1 }, + credentials: { apiKey: "test-key" }, + log: null, + }); + assert.equal(result.success, true); + assert.equal(sawSubmit, true); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/unit/provider-alias-uniqueness.test.ts b/tests/unit/provider-alias-uniqueness.test.ts index 4fb4ba2887..d093d1186c 100644 --- a/tests/unit/provider-alias-uniqueness.test.ts +++ b/tests/unit/provider-alias-uniqueness.test.ts @@ -74,6 +74,14 @@ test("hailuo-web resolves to its own id/alias and does not collide with minimax" assert.equal(resolveProviderId("minimax-cn"), "minimax-cn"); }); +test("freepik is the Magnific Mystic legacy alias, not a second provider id", () => { + assert.equal(resolveProviderId("freepik"), "magnific"); + assert.equal(resolveProviderId("magnific"), "magnific"); + assert.equal(getProviderAlias("magnific"), "freepik"); + assert.ok("magnific" in APIKEY_PROVIDERS); + assert.ok(!("freepik" in APIKEY_PROVIDERS)); +}); + test("no provider id is registered in both the API-key and web-cookie catalogs", () => { // A provider belongs to exactly one auth category; the same id in both catalogs // renders the provider twice in the dashboard (once per section). huggingchat diff --git a/tests/unit/provider-validation-image-only.test.ts b/tests/unit/provider-validation-image-only.test.ts index 7012dfb94e..11a37dee18 100644 --- a/tests/unit/provider-validation-image-only.test.ts +++ b/tests/unit/provider-validation-image-only.test.ts @@ -35,6 +35,11 @@ const imageOnlyProviders = { header: "X-API-Key", value: "topaz-key", }, + magnific: { + url: "https://api.magnific.com/v1/ai/mystic", + header: "x-magnific-api-key", + value: "magnific-key", + }, }; const expectedValidationError = (status: number) => @@ -95,3 +100,20 @@ for (const provider of Object.keys(imageOnlyProviders)) { }); } } + +test("freepik alias validates through the Magnific Mystic endpoint", async () => { + let fetchCalled = false; + globalThis.fetch = async (url, init = {}) => { + fetchCalled = true; + assert.equal(String(url), "https://api.magnific.com/v1/ai/mystic"); + assert.equal((init.headers as Record)["x-magnific-api-key"], "legacy-key"); + return new Response(JSON.stringify({ data: [] }), { status: 200 }); + }; + + const result = await validateProviderApiKey({ provider: "freepik", apiKey: "legacy-key" }); + + assert.equal(result.valid, true); + assert.equal(result.error, null); + assert.notEqual(result.unsupported, true); + assert.equal(fetchCalled, true); +}); diff --git a/tests/unit/redirects-cli-renames.test.ts b/tests/unit/redirects-cli-renames.test.ts index 9fcde5f7c9..b1944534c7 100644 --- a/tests/unit/redirects-cli-renames.test.ts +++ b/tests/unit/redirects-cli-renames.test.ts @@ -38,6 +38,14 @@ test("next.config.mjs has permanent wildcard redirect from /dashboard/agents/:pa ); }); +test("next.config.mjs has permanent redirect from /dashboard/providers/freepik to /dashboard/providers/magnific", () => { + assert.ok( + configSource.includes('source: "/dashboard/providers/freepik"') && + configSource.includes('destination: "/dashboard/providers/magnific"'), + "expected /dashboard/providers/freepik → /dashboard/providers/magnific redirect in next.config.mjs" + ); +}); + test("all 4 CLI redirect entries use permanent: true", () => { // Extract the CLI Pages block const cliBlock = configSource.slice(configSource.indexOf("// CLI Pages — Plano 14 (F9)")); diff --git a/tests/unit/session-affinity-combo-timeout-eviction.test.ts b/tests/unit/session-affinity-combo-timeout-eviction.test.ts index 04a5764957..9dfb36f260 100644 --- a/tests/unit/session-affinity-combo-timeout-eviction.test.ts +++ b/tests/unit/session-affinity-combo-timeout-eviction.test.ts @@ -170,8 +170,13 @@ test("the combo timeout runner aborts with the shared reason constant", () => { ); assert.match( src, - /timeoutController\.abort\(new Error\(COMBO_PER_MODEL_TIMEOUT_REASON\)\)/, - "the runner must use the constant the eviction predicate matches on" + /const abortErr = new Error\(COMBO_PER_MODEL_TIMEOUT_REASON\)/, + "the runner must construct the shared timeout reason" + ); + assert.match( + src, + /timeoutController\.abort\(abortErr\)/, + "the runner must abort with that Error so the eviction predicate matches" ); }); diff --git a/tests/unit/sse-heartbeat.test.ts b/tests/unit/sse-heartbeat.test.ts index 57ecbd9450..2f26b295b7 100644 --- a/tests/unit/sse-heartbeat.test.ts +++ b/tests/unit/sse-heartbeat.test.ts @@ -39,8 +39,19 @@ function decodeChunk(value) { return typeof value === "string" ? value : new TextDecoder().decode(value); } +async function withSseCommentsOn(fn) { + const prev = process.env.OMNIROUTE_SSE_COMMENTS; + process.env.OMNIROUTE_SSE_COMMENTS = "on"; + try { + return await fn(); + } finally { + if (prev === undefined) delete process.env.OMNIROUTE_SSE_COMMENTS; + else process.env.OMNIROUTE_SSE_COMMENTS = prev; + } +} + test("createSseHeartbeatTransform emits SSE comments while preserving stream output", async () => { - await withFakeIntervals(async (intervals) => { + await withSseCommentsOn(() => withFakeIntervals(async (intervals) => { const transform = createSseHeartbeatTransform({ intervalMs: 250 }); const writer = transform.writable.getWriter(); const reader = transform.readable.getReader(); @@ -65,11 +76,11 @@ test("createSseHeartbeatTransform emits SSE comments while preserving stream out assert.equal(emitted[0], 'data: {"chunk":"one"}\n\n'); assert.match(emitted[1], /^: keepalive /); assert.equal(intervals[0].cleared, true); - }); + })); }); test("createSseHeartbeatTransform clears the interval when aborted", async () => { - await withFakeIntervals(async (intervals) => { + await withSseCommentsOn(() => withFakeIntervals(async (intervals) => { const controller = new AbortController(); const transform = createSseHeartbeatTransform({ signal: controller.signal }); const reader = transform.readable.getReader(); @@ -83,7 +94,7 @@ test("createSseHeartbeatTransform clears the interval when aborted", async () => await writer.close(); await reader.cancel(); - }); + })); }); const { shapeForClientFormat } = await import("../../open-sse/utils/sseHeartbeat.ts"); @@ -166,7 +177,7 @@ test("shape: openai-responses-in-progress emits response.in_progress data event" }); test("shape default is comment (back-compat)", async () => { - await withFakeIntervals(async (intervals) => { + await withSseCommentsOn(() => withFakeIntervals(async (intervals) => { const transform = createSseHeartbeatTransform({ intervalMs: 100 }); const writer = transform.writable.getWriter(); const reader = transform.readable.getReader(); @@ -184,7 +195,7 @@ test("shape default is comment (back-compat)", async () => { await pump; assert.match(emitted[0], /^: keepalive /); - }); + })); }); test("intervalMs <= 0 returns passthrough (no setInterval, no heartbeat)", async () => { @@ -224,8 +235,11 @@ test("shapeForClientFormat maps formats correctly", () => { test("no shape collides with stream.ts event: keepalive strip regex", async () => { const shapes = ["comment", "anthropic-ping", "openai-chunk", "openai-responses-in-progress"]; for (const shape of shapes) { - await withFakeIntervals(async (intervals) => { - const transform = createSseHeartbeatTransform({ intervalMs: 100, shape }); + await withSseCommentsOn(() => withFakeIntervals(async (intervals) => { + const transform = createSseHeartbeatTransform({ + intervalMs: 100, + shape, + }); const writer = transform.writable.getWriter(); const reader = transform.readable.getReader(); const emitted = []; @@ -249,6 +263,6 @@ test("no shape collides with stream.ts event: keepalive strip regex", async () = `shape ${shape} produced forbidden line: ${line}` ); } - }); + })); } }); From 769ab62fa3ff46c5afc5552c0faaebff19f206d6 Mon Sep 17 00:00:00 2001 From: Ke Jin Date: Fri, 21 Aug 2026 09:20:54 +0800 Subject: [PATCH 132/135] fix(reasoning): preserve compatible response state (#10574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserves authentic plaintext reasoning continuations across Chat Completions and Responses (streaming + non-streaming), applying one target-aware reasoning transport policy before protocol translation. Fixes #10550. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 39 files): - 446/446 focused node:test tests pass (chat-route-coverage, chatcore-translation-paths, combo-attempt-body-isolation-7847, combo-config, executor-codex, kimi-coding-translator, moonshot-k3, reasoning-cache, response-sanitizer, responses-handler, responses-translation-fixes, strip-reasoning-blobs-agentic-context-1599, translator-openai-responses-req). - 12/12 vitest tests pass (edit-connection-modal-free-models.test.tsx). - check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. - file-size: chatHelpers.ts crossed the frozen cap by +2 lines (irreducible reasoningTransportFallback option threading) — rebaselined 1017->1019 with justification, pushed to the PR branch (fix-in-place), re-validated after a base-drift re-merge against the latest release tip. Co-authored-by: jackjinke --- .../10550-responses-reasoning-transport.md | 1 + config/quality/file-size-baseline.json | 3 +- .../registry/chatgpt-web-codex/index.ts | 1 + .../config/providers/registry/codex/index.ts | 1 + .../providers/registry/grok-cli/index.ts | 1 + .../providers/registry/muse-code/index.ts | 1 + .../config/providers/registry/openai/index.ts | 1 + .../config/providers/registry/xai/index.ts | 2 + open-sse/config/providers/shared.ts | 4 + open-sse/executors/codex.ts | 12 +- open-sse/handlers/chatCore.ts | 28 +- open-sse/handlers/responseSanitizer.ts | 14 +- open-sse/handlers/responseTranslator.ts | 33 +- open-sse/services/reasoningInputPolicy.ts | 339 +++++++++++++ open-sse/services/responsesInputPolicy.ts | 55 -- .../translator/helpers/responsesApiHelper.ts | 17 +- open-sse/translator/index.ts | 161 ++++-- .../translator/request/openai-responses.ts | 22 +- .../request/openai-responses/toResponses.ts | 18 +- .../translator/response/openai-responses.ts | 53 +- open-sse/utils/reasoningContentInjector.ts | 28 +- src/app/(dashboard)/dashboard/combos/page.tsx | 469 ++++++++++-------- .../components/modals/EditConnectionModal.tsx | 11 +- src/shared/validation/schemas/combo.ts | 1 + src/sse/handlers/chat.ts | 7 + src/sse/handlers/chatHelpers.ts | 2 + tests/unit/chat-route-coverage.test.ts | 81 +++ tests/unit/chatcore-translation-paths.test.ts | 366 +++++++++++++- .../combo-attempt-body-isolation-7847.test.ts | 81 +++ tests/unit/combo-config.test.ts | 18 + tests/unit/executor-codex.test.ts | 55 +- tests/unit/kimi-coding-translator.test.ts | 8 +- tests/unit/moonshot-k3.test.ts | 2 +- tests/unit/reasoning-cache.test.ts | 115 ++++- tests/unit/response-sanitizer.test.ts | 26 + tests/unit/responses-handler.test.ts | 2 +- .../unit/responses-translation-fixes.test.ts | 4 +- ...asoning-blobs-agentic-context-1599.test.ts | 299 +++++++++-- .../translator-openai-responses-req.test.ts | 164 +++++- ...edit-connection-modal-free-models.test.tsx | 25 +- 40 files changed, 2075 insertions(+), 456 deletions(-) create mode 100644 changelog.d/fixes/10550-responses-reasoning-transport.md create mode 100644 open-sse/services/reasoningInputPolicy.ts delete mode 100644 open-sse/services/responsesInputPolicy.ts diff --git a/changelog.d/fixes/10550-responses-reasoning-transport.md b/changelog.d/fixes/10550-responses-reasoning-transport.md new file mode 100644 index 0000000000..d34c433deb --- /dev/null +++ b/changelog.d/fixes/10550-responses-reasoning-transport.md @@ -0,0 +1 @@ +- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Combos now drop incompatible continuation reasoning by default and can explicitly skip incompatible targets, while known providers no longer show redundant encrypted-reasoning controls. (#10550) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 7a65a56808..f19fc0ea0b 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.", "_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).", "_rebaseline_2026_08_13_10243_codex_fingerprint_merge": "PR #10243 (xz-dev, Codex OAuth fingerprint convergence) merge into release/v3.8.50: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts crossed the 1000-line new-file cap for the first time (974 on base, 997 on the PR's own branch, 1013 after merging + prettier reflow) purely from combining two independent, already-legitimate feature additions that landed on the same shared UI-helper file — this PR's own Codex fingerprint-mode select/toggle wiring (CODEX_FINGERPRINT_MODE_VALUES, getCodexFingerprintModeLabel, CodexFingerprintModeValue) plus #8949's unrelated Codex account-service-tier helpers merged concurrently on release/v3.8.50. Neither addition alone crosses the cap; git's line-level auto-merge does not detect a threshold crossing. Not modularized as part of this conflict-resolution merge commit (out of scope — this is a merge, not a feature change). Covered by the PR's own tests/unit/codex-fingerprint-convergence.test.ts, tests/unit/executor-codex.test.ts, tests/unit/provider-specific-data-schema.test.ts (all passing post-merge).", "_rebaseline_2026_08_09_8984_api_key_cache_mode": "PR #8984 own growth during the 2026-08-09 rebase: src/lib/db/apiKeys.ts 1529->1545 (+16 = the per-key apiKeys.cacheDefaultMode column + its row parsers and cascade wiring; additive at the existing connection write/read chokepoints). Covered by tests/unit/chatcore-semantic-cache.test.ts. (chatCore.ts stays at the pre-existing base-red ceiling — upstream tip already exceeds the frozen 5042, this PR only adds +2 on top; not re-bumped per the no-inherit-ratchet rule.)", @@ -450,7 +451,7 @@ "src/lib/modelCapabilities.ts": 1006, "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014, "open-sse/config/imageRegistry.ts": 1034, - "src/sse/handlers/chatHelpers.ts": 1017, + "src/sse/handlers/chatHelpers.ts": 1019, "src/shared/middleware/chatBodyAdmission.ts": 1005, "_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file)." }, diff --git a/open-sse/config/providers/registry/chatgpt-web-codex/index.ts b/open-sse/config/providers/registry/chatgpt-web-codex/index.ts index a1ccb6b13c..1c290668f9 100644 --- a/open-sse/config/providers/registry/chatgpt-web-codex/index.ts +++ b/open-sse/config/providers/registry/chatgpt-web-codex/index.ts @@ -14,6 +14,7 @@ export const chatgpt_web_codexProvider: RegistryEntry = { format: "openai-responses", executor: "chatgpt-web-codex", baseUrl: "https://chatgpt.com", + reasoningTransport: "opaque", authType: "apikey", authHeader: "cookie", forceStream: true, diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index 7d6fee4557..6b67797fa3 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -12,6 +12,7 @@ export const codexProvider: RegistryEntry = { format: "openai-responses", executor: "codex", baseUrl: "https://chatgpt.com/backend-api/codex/responses", + reasoningTransport: "opaque", authType: "oauth", authHeader: "bearer", defaultContextLength: 400000, diff --git a/open-sse/config/providers/registry/grok-cli/index.ts b/open-sse/config/providers/registry/grok-cli/index.ts index f257f8d60a..e65ced0e76 100644 --- a/open-sse/config/providers/registry/grok-cli/index.ts +++ b/open-sse/config/providers/registry/grok-cli/index.ts @@ -14,6 +14,7 @@ export const grok_cliProvider: RegistryEntry = { // Keep the generic translate-path contract stable. GrokCliExecutor owns the // official Grok Build upstream URL and always dispatches to /v1/responses. baseUrl: "https://cli-chat-proxy.grok.com/v1/chat/completions", + reasoningTransport: "opaque", modelsUrl: GROK_BUILD_MODELS_URL, clientVersion: getGrokBuildClientVersion(), authType: "oauth", diff --git a/open-sse/config/providers/registry/muse-code/index.ts b/open-sse/config/providers/registry/muse-code/index.ts index 66f59f9f42..37db1e988d 100644 --- a/open-sse/config/providers/registry/muse-code/index.ts +++ b/open-sse/config/providers/registry/muse-code/index.ts @@ -14,6 +14,7 @@ export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEnt id: "muse-code", alias: "mc", passthroughModels: true, + reasoningTransport: "opaque", defaultContextLength: 200000, models: [ { diff --git a/open-sse/config/providers/registry/openai/index.ts b/open-sse/config/providers/registry/openai/index.ts index 63b6a30fa3..60a276a948 100644 --- a/open-sse/config/providers/registry/openai/index.ts +++ b/open-sse/config/providers/registry/openai/index.ts @@ -7,6 +7,7 @@ export const openaiProvider: RegistryEntry = { format: "openai", executor: "default", baseUrl: "https://api.openai.com/v1/chat/completions", + reasoningTransport: "opaque", authType: "apikey", authHeader: "bearer", defaultContextLength: 128000, diff --git a/open-sse/config/providers/registry/xai/index.ts b/open-sse/config/providers/registry/xai/index.ts index efd0a72e3a..dcf9124441 100644 --- a/open-sse/config/providers/registry/xai/index.ts +++ b/open-sse/config/providers/registry/xai/index.ts @@ -12,6 +12,7 @@ export const xaiProvider: RegistryEntry = { // XaiExecutor.buildUrl (open-sse/executors/xai.ts) for models tagged // targetFormat: "openai-responses" below. responsesBaseUrl: "https://api.x.ai/v1/responses", + reasoningTransport: "opaque", authType: "apikey", authHeader: "bearer", models: [ @@ -54,6 +55,7 @@ export const xai_oauthProvider: RegistryEntry = { executor: "xai-oauth", baseUrl: xaiProvider.baseUrl, responsesBaseUrl: xaiProvider.responsesBaseUrl, + reasoningTransport: "opaque", authType: "oauth", authHeader: xaiProvider.authHeader, passthroughModels: true, diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index d65a0240e0..d87250644d 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -108,6 +108,8 @@ export interface RegistryOAuth { pollUrlBase?: string; } +export type ReasoningTransport = "plaintext" | "opaque" | "none"; + export interface RegistryEntry { id: string; alias?: string; @@ -120,6 +122,8 @@ export interface RegistryEntry { /** Override models URL used only for API key validation, not catalog discovery. */ testKeyModelsUrl?: string; responsesBaseUrl?: string; + /** Provider-bound replay format; omitted providers accept portable plaintext reasoning. */ + reasoningTransport?: ReasoningTransport; /** Anthropic-native /v1/messages endpoint (e.g. GitHub Copilot's shim) used * for models tagged `targetFormat: "claude"` on an otherwise openai-format * provider — see registry/github/index.ts. */ diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index b7bc008ec3..ab595c3cbb 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -34,7 +34,7 @@ import { } from "../config/codexIdentity.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts"; -import { applyResponsesInputPolicy } from "../services/responsesInputPolicy.ts"; +import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts"; import { normalizeCodexVerbosity } from "../services/codexVerbosity.ts"; import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts"; import { CORS_HEADERS } from "../utils/cors.ts"; @@ -1389,10 +1389,12 @@ export class CodexExecutor extends BaseExecutor { delete body.session_id; delete body.conversation_id; - applyResponsesInputPolicy( - body, - credentials?.providerSpecificData?.preserveEncryptedReasoning === true - ); + applyReasoningInputPolicy(body, "responses", { + provider: "codex", + preserveEncryptedReasoning: + credentials?.providerSpecificData?.preserveEncryptedReasoning === true, + onIncompatibleReasoning: "drop", + }); if (nativeCodexPassthrough) { return body; diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index dcc281d3e3..beb842bf2d 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -33,7 +33,7 @@ import { assembleStreamingResponseHeaders } from "./chatCore/streamingResponseHe import { storeStreamingSemanticCacheResponse } from "./chatCore/streamingSemanticCacheStore.ts"; import { assembleStreamingPipeline } from "./chatCore/streamingPipeline.ts"; import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts"; -import { applyResponsesInputPolicy } from "../services/responsesInputPolicy.ts"; +import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts"; import { createRoutingEvent, emitRoutingEvent, @@ -511,6 +511,7 @@ export async function handleChatCore({ conversationId = null, modelPinned = false, skipResourcePressureGuard = false, + reasoningTransportFallback = "skip", managedLease = null, }) { let { provider, model, extendedContext } = modelInfo; @@ -1193,11 +1194,30 @@ export async function handleChatCore({ return cacheHit; } - if (targetFormat === FORMATS.OPENAI_RESPONSES && body && typeof body === "object") { - applyResponsesInputPolicy( + const reasoningInputFormat = + sourceFormat === FORMATS.OPENAI_RESPONSES + ? "responses" + : sourceFormat === FORMATS.OPENAI + ? "chat" + : null; + if (reasoningInputFormat && body && typeof body === "object") { + const policy = applyReasoningInputPolicy( body as Record, - credentials?.providerSpecificData?.preserveEncryptedReasoning === true + reasoningInputFormat, + { + provider, + preserveEncryptedReasoning: + credentials?.providerSpecificData?.preserveEncryptedReasoning === true, + onIncompatibleReasoning: reasoningTransportFallback === "drop" ? "drop" : "reject", + } ); + if (policy.incompatibleReasoning) { + trackPendingRequest(model, provider, connectionId, false); + return createErrorResult( + HTTP_STATUS.BAD_REQUEST, + "Reasoning continuation is not compatible with the selected target" + ); + } } body = sanitizeChatRequestBody(body, sourceFormat, targetFormat); diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts index 06d3202f5f..a2210681d7 100644 --- a/open-sse/handlers/responseSanitizer.ts +++ b/open-sse/handlers/responseSanitizer.ts @@ -8,7 +8,10 @@ import { collapseExcessiveNewlines, extractThinkingFromContent, } from "./responseSanitizer/reasoning.ts"; -import { applyCacheHitTokensToUsage, applyCacheHitTokensToResponsesUsage } from "./responseSanitizer/cacheHitTokens.ts"; +import { + applyCacheHitTokensToUsage, + applyCacheHitTokensToResponsesUsage, +} from "./responseSanitizer/cacheHitTokens.ts"; export { extractThinkingFromContent, shouldParseTextualReasoningTags, @@ -31,7 +34,9 @@ const ALLOWED_USAGE_FIELDS = new Set([ "total_tokens", "cached_tokens", "prompt_tokens_details", - "completion_tokens_details", "cache_read_input_tokens", "cache_creation_input_tokens", + "completion_tokens_details", + "cache_read_input_tokens", + "cache_creation_input_tokens", // Keep through sanitize → applyClientUsageBuffer so heuristic web usage is // not inflated by the default USAGE_TOKEN_BUFFER (2000). "estimated", @@ -550,7 +555,7 @@ function sanitizeResponsesUsage(usage: unknown): unknown { !(toRecord(normalized.input_tokens_details) ?? {}).cached_tokens ) { normalized.input_tokens_details = { - ...(normalized.input_tokens_details as Record || {}), + ...((normalized.input_tokens_details as Record) || {}), cached_tokens: normalized.prompt_cache_hit_tokens, }; } @@ -562,7 +567,7 @@ function sanitizeResponsesUsage(usage: unknown): unknown { !(toRecord(normalized.input_tokens_details) ?? {}).cached_tokens ) { normalized.input_tokens_details = { - ...(normalized.input_tokens_details as Record || {}), + ...((normalized.input_tokens_details as Record) || {}), cached_tokens: normalized.cache_read_input_tokens, }; } @@ -863,6 +868,7 @@ function sanitizeResponsesOutputItem(item: unknown, index: number): JsonRecord | : []; return { + ...itemRecord, id: toString(itemRecord.id) || `rs_${index}`, type: "reasoning", summary, diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index 407393966d..01bdd4c14a 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -10,6 +10,7 @@ import { caseInsensitiveToolNameLookup, restoreOpenAIToolNames, } from "../translator/helpers/toolCallHelper.ts"; +import { extractReplayableResponsesReasoningText } from "../services/reasoningInputPolicy.ts"; import { sanitizeToolId } from "../translator/helpers/schemaCoercion.ts"; type JsonRecord = Record; @@ -178,7 +179,8 @@ export function translateNonStreamingResponse( const messageSelection = findBestMessageText(output); let textContent = messageSelection.text; - let reasoningContent = ""; + let replayableReasoningContent = ""; + let reasoningSummary = ""; const toolCalls: JsonRecord[] = []; for (const item of output) { @@ -192,16 +194,22 @@ export function translateNonStreamingResponse( if (partObj.type === "summary_text" && typeof partObj.text === "string") { // #9500 — reasoning summary parts are discrete segments; join with "\n\n" // (matches extractThinkingFromContent convention) so they don't glue back-to-back. - reasoningContent += reasoningContent ? `\n\n${partObj.text}` : partObj.text; + reasoningSummary += reasoningSummary ? `\n\n${partObj.text}` : partObj.text; } } - } else if (itemObj.type === "reasoning" && Array.isArray(itemObj.summary)) { - for (const part of itemObj.summary) { - const partObj = toRecord(part); - if (partObj.type === "summary_text" && typeof partObj.text === "string") { - // #9500 — reasoning summary parts are discrete segments; join with "\n\n" - // (matches extractThinkingFromContent convention) so they don't glue back-to-back. - reasoningContent += reasoningContent ? `\n\n${partObj.text}` : partObj.text; + } else if (itemObj.type === "reasoning") { + const replayable = extractReplayableResponsesReasoningText(itemObj); + if (replayable) { + replayableReasoningContent += replayableReasoningContent + ? `\n\n${replayable}` + : replayable; + } + if (Array.isArray(itemObj.summary)) { + for (const part of itemObj.summary) { + const partObj = toRecord(part); + if (partObj.type === "summary_text" && typeof partObj.text === "string") { + reasoningSummary += reasoningSummary ? `\n\n${partObj.text}` : partObj.text; + } } } } else if (itemObj.type === "function_call") { @@ -238,8 +246,11 @@ export function translateNonStreamingResponse( if (textContent) { message.content = textContent; } - if (reasoningContent) { - message.reasoning_content = reasoningContent; + if (replayableReasoningContent) { + message.reasoning_content = replayableReasoningContent; + } + if (reasoningSummary) { + message.reasoning_summary = [{ type: "summary_text", text: reasoningSummary }]; } if (toolCalls.length > 0) { message.tool_calls = toolCalls; diff --git a/open-sse/services/reasoningInputPolicy.ts b/open-sse/services/reasoningInputPolicy.ts new file mode 100644 index 0000000000..71a283a706 --- /dev/null +++ b/open-sse/services/reasoningInputPolicy.ts @@ -0,0 +1,339 @@ +import { REGISTRY } from "../config/providerRegistry.ts"; +import type { ReasoningTransport } from "../config/providerRegistry.ts"; + +type JsonRecord = Record; + +const REASONING_TRANSPORTS = new Map(); +for (const [id, entry] of Object.entries(REGISTRY)) { + if (!entry.reasoningTransport) continue; + REASONING_TRANSPORTS.set(id.toLowerCase(), entry.reasoningTransport); + if (entry.alias) { + REASONING_TRANSPORTS.set(entry.alias.toLowerCase(), entry.reasoningTransport); + } +} + +const CHAT_PLAINTEXT_REASONING_FIELDS = [ + "reasoning_content", + "reasoning", + "reasoning_text", + "thinking", + "thought", +] as const; + +export type ReasoningInputFormat = "chat" | "responses"; + +export interface ReasoningStateInspection { + hasPlaintext: boolean; + hasOpaque: boolean; +} + +export interface ReasoningInputPolicyOptions { + provider?: string | null; + preserveEncryptedReasoning?: boolean; + onIncompatibleReasoning?: "reject" | "drop"; +} + +export interface ReasoningInputPolicyResult { + incompatibleReasoning: boolean; +} + +export function resolveReasoningTransport( + provider: string | null | undefined, + preserveEncryptedReasoning = false +): ReasoningTransport { + const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : ""; + const transport = REASONING_TRANSPORTS.get(normalized); + return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext"); +} + +export function createReasoningTransportIncompatibleError(): Error & { + statusCode: number; + errorType: string; +} { + const error = new Error( + "Reasoning continuation is not compatible with the selected target" + ) as Error & { statusCode: number; errorType: string }; + error.statusCode = 400; + error.errorType = "reasoning_transport_incompatible"; + return error; +} + +function asRecord(value: unknown): JsonRecord | null { + return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null; +} + +function isNonEmptyString(value: unknown): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +function isSummaryDetail(record: JsonRecord): boolean { + const type = typeof record.type === "string" ? record.type.toLowerCase() : ""; + return ( + type.includes("summary") || record.summary !== undefined || record.summary_text !== undefined + ); +} + +function hasPlaintextReasoning(record: JsonRecord): boolean { + return ( + Array.isArray(record.content) && + record.content.some((part) => { + const value = asRecord(part); + return value?.type === "reasoning_text" && isNonEmptyString(value.text); + }) + ); +} + +function hasChatPlaintextReasoning(record: JsonRecord): boolean { + if (CHAT_PLAINTEXT_REASONING_FIELDS.some((field) => isNonEmptyString(record[field]))) { + return true; + } + if (!Array.isArray(record.reasoning_details)) return false; + return record.reasoning_details.some((detail) => { + const value = asRecord(detail); + return Boolean( + value && + !isSummaryDetail(value) && + (isNonEmptyString(value.text) || isNonEmptyString(value.content)) + ); + }); +} + +/** + * Returns only provider-authentic plaintext continuation state. Display summaries + * are excluded, and a record carrying opaque state is never cross-converted. + */ +export function extractReplayableResponsesReasoningText(value: unknown): string { + const record = asRecord(value); + if (!record || record.type !== "reasoning" || hasOpaqueReasoningState(record)) return ""; + if (!Array.isArray(record.content)) return ""; + + return record.content + .map((part) => { + const content = asRecord(part); + return content?.type === "reasoning_text" && typeof content.text === "string" + ? content.text + : ""; + }) + .filter((text) => text.trim().length > 0) + .join("\n\n"); +} + +export function hasOpaqueReasoningState(record: JsonRecord): boolean { + return ( + isNonEmptyString(record.encrypted_content) || + record.signature !== undefined || + record.format !== undefined + ); +} + +function hasOpaqueReasoningDetail(value: unknown): boolean { + const record = asRecord(value); + if (!record) return false; + const type = typeof record.type === "string" ? record.type.toLowerCase() : ""; + return ( + hasOpaqueReasoningState(record) || + ((type.includes("encrypted") || type.includes("opaque")) && isNonEmptyString(record.data)) + ); +} + +function hasChatOpaqueReasoning(record: JsonRecord): boolean { + return ( + hasOpaqueReasoningState(record) || + (Array.isArray(record.reasoning_details) && + record.reasoning_details.some(hasOpaqueReasoningDetail)) + ); +} + +export function inspectChatReasoning(messages: unknown): ReasoningStateInspection { + const inspection: ReasoningStateInspection = { hasPlaintext: false, hasOpaque: false }; + if (!Array.isArray(messages)) return inspection; + + for (const message of messages) { + const record = asRecord(message); + if (!record || record.role !== "assistant") continue; + inspection.hasPlaintext ||= hasChatPlaintextReasoning(record); + inspection.hasOpaque ||= hasChatOpaqueReasoning(record); + if (inspection.hasPlaintext && inspection.hasOpaque) break; + } + return inspection; +} + +export function inspectResponsesReasoning(input: unknown): ReasoningStateInspection { + const inspection: ReasoningStateInspection = { hasPlaintext: false, hasOpaque: false }; + if (!Array.isArray(input)) return inspection; + + for (const item of input) { + const record = asRecord(item); + if (!record || record.type !== "reasoning") continue; + inspection.hasPlaintext ||= hasPlaintextReasoning(record); + inspection.hasOpaque ||= hasOpaqueReasoningState(record); + if (inspection.hasPlaintext && inspection.hasOpaque) break; + } + return inspection; +} + +function isReasoningCompatible( + inspection: ReasoningStateInspection, + transport: ReasoningTransport +): boolean { + if (!inspection.hasPlaintext && !inspection.hasOpaque) return true; + if (transport === "plaintext") return !inspection.hasOpaque; + if (transport === "opaque") return !inspection.hasPlaintext; + return false; +} + +function stripOpaqueFields(record: JsonRecord): void { + delete record.encrypted_content; + delete record.signature; + delete record.format; + delete record.data; +} + +function stripChatReasoningDetails(details: unknown[], transport: ReasoningTransport): unknown[] { + return details.flatMap((detail) => { + const record = asRecord(detail); + if (!record) return [detail]; + + const plaintext = + !isSummaryDetail(record) && + (isNonEmptyString(record.text) || isNonEmptyString(record.content)); + const opaque = hasOpaqueReasoningDetail(record); + if ((!plaintext || transport === "plaintext") && (!opaque || transport === "opaque")) { + return [detail]; + } + + const next = { ...record }; + if (plaintext && transport !== "plaintext") { + delete next.text; + delete next.content; + } + if (opaque && transport !== "opaque") stripOpaqueFields(next); + const remainingKeys = Object.keys(next).filter((key) => key !== "type"); + return remainingKeys.length > 0 ? [next] : []; + }); +} + +function dropIncompatibleChatReasoning( + messages: unknown[], + transport: ReasoningTransport +): unknown[] { + return messages.map((message) => { + const record = asRecord(message); + if (!record || record.role !== "assistant") return message; + const next = { ...record }; + if (transport !== "plaintext") { + for (const field of CHAT_PLAINTEXT_REASONING_FIELDS) delete next[field]; + } + if (transport !== "opaque") stripOpaqueFields(next); + if (Array.isArray(record.reasoning_details)) { + const details = stripChatReasoningDetails(record.reasoning_details, transport); + if (details.length > 0) next.reasoning_details = details; + else delete next.reasoning_details; + } + return next; + }); +} + +function hasDisplaySummary(record: JsonRecord): boolean { + return record.summary !== undefined || record.summary_text !== undefined; +} + +function dropIncompatibleResponsesReasoning( + record: JsonRecord, + transport: ReasoningTransport +): JsonRecord | null { + const next = { ...record }; + if (transport !== "plaintext" && Array.isArray(record.content)) { + const content = record.content.filter((part) => asRecord(part)?.type !== "reasoning_text"); + if (content.length > 0) next.content = content; + else delete next.content; + } + if (transport !== "opaque") stripOpaqueFields(next); + const stillActive = hasPlaintextReasoning(next) || hasOpaqueReasoningState(next); + return stillActive || hasDisplaySummary(next) ? next : null; +} + +function sanitizeResponsesInput( + input: unknown[], + transport: ReasoningTransport, + dropIncompatible: boolean, + stripOrphanedSummaries: boolean +): unknown[] { + const filtered: unknown[] = []; + for (const item of input) { + if (typeof item === "string") continue; + const record = asRecord(item); + if (!record) { + filtered.push(item); + continue; + } + if (record.type === "item_reference") continue; + + if (record.type === "reasoning") { + const next = dropIncompatible + ? dropIncompatibleResponsesReasoning(record, transport) + : { ...record }; + if (!next) continue; + const hasPlaintext = hasPlaintextReasoning(next); + const hasOpaque = hasOpaqueReasoningState(next); + if (!hasPlaintext && !hasOpaque && (!hasDisplaySummary(next) || stripOrphanedSummaries)) { + continue; + } + if (!hasOpaque && typeof next.id === "string") delete next.id; + filtered.push(next); + continue; + } + + const cloned = { ...record }; + if (typeof cloned.id === "string") delete cloned.id; + filtered.push(cloned); + } + return filtered; +} + +/** + * Applies one protocol-independent compatibility decision before request translation. + * Plaintext is portable by default; opaque state requires an explicit target declaration. + * Display summaries do not affect compatibility; stateless input drops orphan summaries. + */ +export function applyReasoningInputPolicy( + body: Record, + inputFormat: ReasoningInputFormat, + options: ReasoningInputPolicyOptions = {} +): ReasoningInputPolicyResult { + const transport = resolveReasoningTransport(options.provider, options.preserveEncryptedReasoning); + const inspection = + inputFormat === "responses" + ? inspectResponsesReasoning(body.input) + : inspectChatReasoning(body.messages); + const incompatibleReasoning = !isReasoningCompatible(inspection, transport); + + if (incompatibleReasoning && options.onIncompatibleReasoning !== "drop") { + return { incompatibleReasoning: true }; + } + + if (inputFormat === "chat") { + if (incompatibleReasoning && Array.isArray(body.messages)) { + body.messages = dropIncompatibleChatReasoning(body.messages, transport); + } + return { incompatibleReasoning: false }; + } + + if (Array.isArray(body.input) && body.input.length === 0) { + body.input = [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "continue" }], + }, + ]; + } + if (!Array.isArray(body.input)) return { incompatibleReasoning: false }; + body.input = sanitizeResponsesInput( + body.input, + transport, + incompatibleReasoning, + body.store === false + ); + return { incompatibleReasoning: false }; +} diff --git a/open-sse/services/responsesInputPolicy.ts b/open-sse/services/responsesInputPolicy.ts deleted file mode 100644 index d80dcc7bec..0000000000 --- a/open-sse/services/responsesInputPolicy.ts +++ /dev/null @@ -1,55 +0,0 @@ -type JsonRecord = Record; - -const SERVER_ITEM_ID_PATTERN = /^(rs|fc|resp|msg)_/; - -/** - * Applies the persistence-independent policy for replayed Responses input items. - * Stored references can only be resolved by the upstream that created them, so - * they are always removed. Self-contained encrypted reasoning is retained only - * when the selected connection explicitly opts in. - */ -export function applyResponsesInputPolicy( - body: Record, - preserveEncryptedReasoning = false -): void { - if (Array.isArray(body.input) && body.input.length === 0) { - body.input = [ - { - type: "message", - role: "user", - content: [{ type: "input_text", text: "continue" }], - }, - ]; - } - - if (!Array.isArray(body.input)) return; - - body.input = body.input.filter((item) => { - if (typeof item === "string" && SERVER_ITEM_ID_PATTERN.test(item)) { - return false; - } - - const record = - item && typeof item === "object" && !Array.isArray(item) ? (item as JsonRecord) : null; - if (!record) return true; - - if (record.type === "item_reference") { - return false; - } - - if ( - record.type === "reasoning" && - (!preserveEncryptedReasoning || - typeof record.encrypted_content !== "string" || - record.encrypted_content.trim().length === 0) - ) { - return false; - } - - if (typeof record.id === "string" && SERVER_ITEM_ID_PATTERN.test(record.id)) { - delete record.id; - } - - return true; - }); -} diff --git a/open-sse/translator/helpers/responsesApiHelper.ts b/open-sse/translator/helpers/responsesApiHelper.ts index 625daf174f..a6c0ea29d5 100644 --- a/open-sse/translator/helpers/responsesApiHelper.ts +++ b/open-sse/translator/helpers/responsesApiHelper.ts @@ -3,6 +3,7 @@ * Delegates to the canonical translator to avoid logic duplication. */ import { requiresReasoningReplay } from "../../services/reasoningCache.ts"; +import { requiresAuthenticReasoningContent } from "../../utils/reasoningContentInjector.ts"; import { openaiResponsesToOpenAIRequest } from "../request/openai-responses.ts"; import { toRecord } from "../request/openai-responses/helpers.ts"; @@ -23,13 +24,15 @@ export function convertResponsesApiFormat( credentials && typeof credentials === "object" && !Array.isArray(credentials) ? (credentials as Record) : {}; - const translationCredentials = requiresReasoningReplay({ - provider: String(provider ?? ""), - model: String(model ?? ""), - allowLegacyFallback: false, - }) - ? { ...credentialRecord, _preserveReasoningContent: true } - : credentials; + const translationCredentials = + requiresAuthenticReasoningContent(provider, model) || + requiresReasoningReplay({ + provider: String(provider ?? ""), + model: String(model ?? ""), + allowLegacyFallback: false, + }) + ? { ...credentialRecord, _preserveReasoningContent: true } + : credentials; const converted = openaiResponsesToOpenAIRequest( requestedModel, body, diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index 979fce6e5f..501d2ffdbc 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -202,6 +202,88 @@ function requiresReasoningContentPresence(provider: unknown, model: unknown): bo return normalizedProvider === "xiaomi-mimo" || /(^|\/)mimo/i.test(normalizedModel); } +type OpenAIReplayOptions = { + canReplayReasoningOnly: boolean; + requiresExplicitReasoningReplay: boolean; + provider: string; + model: string; + reasoningCacheScope?: string | null; +}; + +function replayOpenAIReasoningMessage( + messages: Array>, + messageIndex: number, + options: OpenAIReplayOptions +): void { + const message = messages[messageIndex]; + if (!message || message.role !== "assistant") return; + + // Moonshot `partial` messages are output prefixes, not completed prior turns. + if (message.partial === true) { + if (message.reasoning_content === "") delete message.reasoning_content; + return; + } + + if ( + !hasNonEmptyReasoningContent(message) && + typeof message.reasoning === "string" && + message.reasoning.trim().length > 0 + ) { + message.reasoning_content = message.reasoning; + } + + const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : []; + const hasToolCalls = toolCalls.length > 0; + const shouldReplayReasoningOnly = + !hasToolCalls && options.canReplayReasoningOnly && !hasNonEmptyReasoningContent(message); + + if (!hasToolCalls && !shouldReplayReasoningOnly) { + if ( + message.reasoning_content === "" || + isInternalReasoningPlaceholder(message.reasoning_content) + ) { + delete message.reasoning_content; + } + return; + } + + if (hasNonEmptyReasoningContent(message)) { + if (!isInternalReasoningPlaceholder(message.reasoning_content)) return; + delete message.reasoning_content; + } + + const firstToolCall = + toolCalls[0] && typeof toolCalls[0] === "object" && !Array.isArray(toolCalls[0]) + ? (toolCalls[0] as Record) + : null; + const cacheKey = hasToolCalls + ? typeof firstToolCall?.id === "string" + ? firstToolCall.id + : "" + : buildAssistantMessageCacheKey(options.reasoningCacheScope, messages, messageIndex); + if (cacheKey) { + const cached = lookupReasoning(cacheKey); + if (cached) { + message.reasoning_content = cached; + recordReplay(); + return; + } + } + + if (options.requiresExplicitReasoningReplay) { + if (message.reasoning_content === "") delete message.reasoning_content; + return; + } + + if ((hasToolCalls || shouldReplayReasoningOnly) && !message.reasoning_content) { + if (requiresReasoningContentPresence(options.provider, options.model)) { + message.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER; + } else { + delete message.reasoning_content; + } + } +} + /** @param options.normalizeToolCallId - When true, use 9-char tool call ids (e.g. Mistral); when false, leave ids as-is */ /** @param options.preserveDeveloperRole - undefined/true: keep developer for OpenAI format (default); false: map to system */ /** @param options.preserveCacheControl - When true, preserve client-side cache_control markers (for Claude Code, etc.) */ @@ -321,6 +403,25 @@ export function translateRequest( result.messages = hoistLeadingSystemMessage(result.messages, provider); } + if ( + sourceFormat === FORMATS.OPENAI && + targetFormat === FORMATS.OPENAI_RESPONSES && + isReasoner && + Array.isArray(result.messages) + ) { + const messages = result.messages as Array>; + const replayOptions: OpenAIReplayOptions = { + canReplayReasoningOnly: isReasoningOnlyReplayTarget(normalizedProvider, normalizedModel), + requiresExplicitReasoningReplay, + provider: normalizedProvider, + model: normalizedModel, + reasoningCacheScope: options?.reasoningCacheScope, + }; + for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) { + replayOpenAIReasoningMessage(messages, messageIndex, replayOptions); + } + } + // If same format, skip translation steps if (sourceFormat !== targetFormat) { // Check for direct translation path first (e.g., Claude → Gemini) @@ -619,59 +720,13 @@ export function translateRequest( } // ── OpenAI-format message ── - // Skip if client already provided real reasoning_content. The internal - // replay placeholder is NOT real reasoning: drop it and fall through to - // the cache lookup so it can be replaced with genuine cached reasoning. - // Forwarding it makes the model continue its chain of thought from that - // text (echo → empty stop), and the echo re-poisons cache + client - // history (#9573). - if (hasNonEmptyReasoningContent(msg)) { - if (!isInternalReasoningPlaceholder(msg.reasoning_content)) { - continue; - } - delete msg.reasoning_content; - } - - const cacheKey = hasToolCalls - ? msg.tool_calls[0]?.id - : buildAssistantMessageCacheKey( - options?.reasoningCacheScope, - result.messages, - messageIndex - ); - if (cacheKey) { - const cached = lookupReasoning(cacheKey); - if (cached) { - msg.reasoning_content = cached; - recordReplay(); - continue; - } - } - - // Native Moonshot K3/K2.7 accepts only the real prior reasoning. If it - // was not supplied and the cache missed, leave it absent so upstream can - // enforce its contract instead of corrupting history with a placeholder. - if (requiresExplicitReasoningReplay) { - if (msg.reasoning_content === "") delete msg.reasoning_content; - continue; - } - - // Cache miss fallback — previously injected a non-empty placeholder - // (NON_ANTHROPIC_THINKING_PLACEHOLDER) to dodge an alleged DeepSeek V4 400 - // on missing reasoning_content. The placeholder is the root cause of this - // bug: the model echoes it as its own reasoning and stops (empty turns), - // and the echo re-poisons the cache + client history (#9573). Empirically, - // deepseek-v4-flash accepts an ABSENT reasoning_content field (the 400 is - // specific to empty-string, and even that is endpoint-dependent). Omit - // the field instead; providers that genuinely enforce the contract - // (kimi-coding, moonshot reasoning replay) have their own paths above. - if ((hasToolCalls || shouldReplayReasoningOnly) && !msg.reasoning_content) { - if (requiresReasoningContentPresence(normalizedProvider, normalizedModel)) { - msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER; - } else { - delete msg.reasoning_content; - } - } + replayOpenAIReasoningMessage(result.messages, messageIndex, { + canReplayReasoningOnly, + requiresExplicitReasoningReplay, + provider: normalizedProvider, + model: normalizedModel, + reasoningCacheScope: options?.reasoningCacheScope, + }); } } else if ( !isReasoner && diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index df65ab5f3c..b886650252 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -8,6 +8,11 @@ import { isOpenAIResponsesStoreEnabled } from "@/lib/providers/requestDefaults"; import { FORMATS } from "../formats.ts"; import { register } from "../registry.ts"; import { normalizeResponsesInputForChat } from "../../utils/responsesInputNormalization.ts"; +import { + createReasoningTransportIncompatibleError, + hasOpaqueReasoningState, + extractReplayableResponsesReasoningText, +} from "../../services/reasoningInputPolicy.ts"; import { getRegisteredProviders, requiresPlainStringContent, @@ -73,14 +78,6 @@ function toolOutputContentToString(output: unknown): string { return parts.join("\n"); } -function getReasoningSummaryText(item: JsonRecord): string { - if (!Array.isArray(item.summary)) return ""; - return item.summary - .map((part) => toString(toRecord(part).text)) - .filter((text) => text.length > 0) - .join("\n\n"); -} - function appendReasoningContent(current: unknown, next: string): string { const existing = typeof current === "string" ? current : ""; return existing ? `${existing}\n\n${next}` : next; @@ -456,10 +453,13 @@ export function openaiResponsesToOpenAIRequest( } if (itemType === "reasoning") { - // Responses reasoning summaries are normally display metadata. Preserve them only - // when the routed upstream explicitly requires prior reasoning to continue a turn. + // Only genuine plaintext reasoning can cross into Chat reasoning_content. + // Opaque encrypted state and its display summary have no Chat replay form. + if (preserveReasoningContent && hasOpaqueReasoningState(item)) { + throw createReasoningTransportIncompatibleError(); + } if (preserveReasoningContent) { - const reasoning = getReasoningSummaryText(item); + const reasoning = extractReplayableResponsesReasoningText(item); if (reasoning) { if (currentAssistantMsg) { currentAssistantMsg.reasoning_content = appendReasoningContent( diff --git a/open-sse/translator/request/openai-responses/toResponses.ts b/open-sse/translator/request/openai-responses/toResponses.ts index f91b66f918..bee5efab1a 100644 --- a/open-sse/translator/request/openai-responses/toResponses.ts +++ b/open-sse/translator/request/openai-responses/toResponses.ts @@ -4,6 +4,8 @@ * Extracted verbatim from openai-responses.ts. Registration stays in the host. */ import { isOpenAIResponsesStoreEnabled } from "@/lib/providers/requestDefaults"; +import { isInternalReasoningPlaceholder } from "../../../utils/reasoningPlaceholder.ts"; +import { getReadableReasoningValue } from "../../../utils/reasoningFields.ts"; import { generateToolCallId } from "../../helpers/toolCallHelper.ts"; import { JsonRecord, @@ -192,12 +194,18 @@ export function openaiToOpenAIResponsesRequest( // Convert assistant messages if (role === "assistant") { - // Skip reasoning_content — OpenAI Responses API requires server-generated - // rs_* IDs for reasoning items. Synthesizing client-side IDs (e.g. reasoning_N) - // causes 400 errors from Responses-compatible upstreams. (#224) - - // Skip thinking blocks in array content — same rs_* ID constraint applies + const reasoning = getReadableReasoningValue(msg).trim(); + if (reasoning && !isInternalReasoningPlaceholder(reasoning)) { + // Compatibility is decided before protocol translation; this adapter + // only encodes the surviving portable plaintext state. + input.push({ + type: "reasoning", + content: [{ type: "reasoning_text", text: reasoning }], + }); + } + // Thinking blocks remain display-only here. They do not prove that the + // selected target accepts their provider-specific replay representation. // Build assistant output content const outputContent: unknown[] = []; if (typeof msg.content === "string" && msg.content) { diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index 09ff1c8d7c..0c59fac4fb 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -12,6 +12,7 @@ import { isInternalReasoningPlaceholder, stripInternalReasoningPlaceholder, } from "../../utils/reasoningPlaceholder.ts"; +import { extractReplayableResponsesReasoningText } from "../../services/reasoningInputPolicy.ts"; import { normalizeToolName, stripEmptyOptionalToolArgs, @@ -542,7 +543,8 @@ function emitToolCall(state, emit, tc) { const toolName = state.funcNames[tcIdx] || funcName || ""; const lowerName = toolName.toLowerCase(); const isCustomTool = - ((lowerName === "apply_patch" || lowerName === "applypatch") && !state.toolSchemas?.has?.(toolName)) || + ((lowerName === "apply_patch" || lowerName === "applypatch") && + !state.toolSchemas?.has?.(toolName)) || state.customToolNames?.has?.(toolName) === true; if (!state.funcCallIds[tcIdx] && newCallId) state.funcCallIds[tcIdx] = newCallId; @@ -614,7 +616,8 @@ function closeToolCall(state, emit, idx, recordAsCompleted = true) { // same classification independently for their respective add/close call sites). const lowerName = toolName.toLowerCase(); const isCustomTool = - ((lowerName === "apply_patch" || lowerName === "applypatch") && !state.toolSchemas?.has?.(toolName)) || + ((lowerName === "apply_patch" || lowerName === "applypatch") && + !state.toolSchemas?.has?.(toolName)) || state.customToolNames?.has?.(toolName) === true; let funcItem; @@ -1042,6 +1045,17 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { return null; } + if (eventType === "response.output_item.done" && data.item?.type === "reasoning") { + const replayableReasoning = extractReplayableResponsesReasoningText(data.item); + if (replayableReasoning) { + const accumulated = + typeof state.accumulatedReasoning === "string" ? state.accumulatedReasoning : ""; + state.accumulatedReasoning = accumulated + ? `${accumulated}\n\n${replayableReasoning}` + : replayableReasoning; + } + } + // Function call done — emit args chunk from item.arguments when no deltas were received, // then advance the tool-call index. This handles Codex Responses API payloads that // carry the complete arguments only in output_item.done (no preceding delta events). @@ -1055,6 +1069,28 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { const shouldNormalizeArguments = toolName === "Agent"; state.currentToolCallNeedsNormalization = shouldNormalizeArguments; + if (toolName && state.toolCalls instanceof Map) { + const completedArguments = + typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : buffered; + const normalizedArguments = stripEmptyOptionalToolArgs( + completedArguments, + toolName, + toolSchema + ); + state.toolCalls.set(currentIndex, { + id: callId, + index: currentIndex, + type: "function", + function: { + name: toolName, + arguments: + typeof normalizedArguments === "string" + ? normalizedArguments + : JSON.stringify(normalizedArguments ?? {}), + }, + }); + } + // Track this call_id so response.completed doesn't synthesize a duplicate if (!state.toolCallIdsSeen) state.toolCallIdsSeen = new Set(); if (callId) state.toolCallIdsSeen.add(callId); @@ -1314,12 +1350,8 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { return buildResponsesReasoningDeltaChunk(state, deltaText); } - // #5786 — reasoning summary exposed ONLY as a terminal snapshot on - // `response.output_item.done` (no preceding reasoning_summary_text.delta events — e.g. - // Codex reasoning models that surface the summary once at item close). Without this the - // reasoning channel is silently dropped and never reaches the client's thinking panel. - // Only synthesize when NO reasoning delta was already streamed for this item, so normal - // delta streams are never duplicated. + // Some providers expose completed reasoning only on `response.output_item.done`. + // Synthesize one Chat reasoning delta only when no delta was already emitted. if (eventType === "response.output_item.done" && data.item?.type === "reasoning") { const item = data.item; const itemId = item.id != null ? String(item.id) : ""; @@ -1334,6 +1366,11 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { !(state.reasoningItemsWithDelta instanceof Set && state.reasoningItemsWithDelta.size > 0); if (emittedForItem || emittedWithoutItemId) return null; + const replayableReasoning = extractReplayableResponsesReasoningText(item); + if (replayableReasoning) { + return buildResponsesReasoningDeltaChunk(state, replayableReasoning); + } + // #7176/#7243: only synthesize from real upstream plaintext — never mutate // `item` and never fabricate placeholder text for encrypted-only reasoning. const summaryText = getVisibleResponsesReasoningSummaryText(item); diff --git a/open-sse/utils/reasoningContentInjector.ts b/open-sse/utils/reasoningContentInjector.ts index 375057cf77..a1d69ebf6d 100644 --- a/open-sse/utils/reasoningContentInjector.ts +++ b/open-sse/utils/reasoningContentInjector.ts @@ -13,8 +13,6 @@ * that proxy to thinking-mode models. */ -import { requiresReasoningReplay } from "../services/reasoningCache.ts"; - const PLACEHOLDER = " "; type JsonRecord = Record; @@ -31,6 +29,26 @@ const THINKING_MODEL_PATTERNS: RegExp[] = [ /\bminimax\b/i, /\bmimo\b/i, // xiaomi-tokenplan mimo family (e.g. xiaomi-tokenplan/mimo-v2.5-pro) ]; +const K3_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)(?:kimi-)?k3(?:$|-)/i; +const NATIVE_K27_AUTHENTIC_REASONING_PATTERN = /(?:^|\/)kimi-k2\.7-code(?:$|-)/i; + +/** + * K3 requires authentic reasoning regardless of which provider serves it. + * Native Moonshot K2.7 retains the same preserved-thinking contract. Empty + * protocol markers remain valid only after client content and replay miss. + */ +export function requiresAuthenticReasoningContent(provider: unknown, model: unknown): boolean { + const normalizedModel = String(model ?? "").trim(); + if (K3_AUTHENTIC_REASONING_PATTERN.test(normalizedModel)) return true; + + const normalizedProvider = String(provider ?? "") + .trim() + .toLowerCase(); + return ( + (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && + NATIVE_K27_AUTHENTIC_REASONING_PATTERN.test(normalizedModel) + ); +} export function isThinkingMessageModel(model: string | undefined | null): boolean { if (!model || typeof model !== "string") return false; @@ -46,11 +64,7 @@ export function shouldInjectReasoningContentPlaceholder( .toLowerCase(); return ( (normalizedProvider === "moonshot" || normalizedProvider === "kimi") && - !requiresReasoningReplay({ - provider: normalizedProvider, - model: String(model ?? ""), - allowLegacyFallback: false, - }) && + !requiresAuthenticReasoningContent(normalizedProvider, model) && isThinkingMessageModel(model) ); } diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index f456d8c6d9..fd9d1e3b7e 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -188,6 +188,8 @@ const ADVANCED_FIELD_HELP_FALLBACK = { "Delay between set-level retry attempts, giving transient issues time to resolve.", nestedComboMode: "How references to other combos are handled. Flatten expands a combo ref into this combo's target list (legacy). Execute treats a combo ref as a black-box target: the parent strategy selects the child combo, then the child runs its own strategy and retries.", + reasoningTransportFallback: + "What to do when the next combo target cannot accept the original reasoning transport. Drop is the default: it removes reasoning state and tries the target. Skip leaves the request body untouched and falls through.", }; const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ @@ -3230,213 +3232,232 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo {builderSelectionMode === "step" && ( <>
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
-
- - {builderConnectionId === COMBO_BUILDER_AUTO_CONNECTION && - selectedBuilderConnections.length > 1 ? ( -
- -
- {selectedBuilderConnections.map((connection) => { - const checked = builderAllowedConnectionIds.includes(connection.id); - return ( - - ); - })} +
+ + +
-

- {getI18nOrFallback( - t, - "builderRestrictAccountsHint", - "Leave empty to use the whole active pool. When selected, round-robin / weighted picks stay within this subset of accounts." - )} -

-
- ) : null} - {isExpertMode ? ( -
- - {builderHasDuplicate && ( - - {getI18nOrFallback( - t, - "builderDuplicateExact", - "This exact provider/model/account step is already in the combo." - )} - - )} -
- ) : ( -
-

- {getI18nOrFallback(t, "builderPreview", "Current step preview")} -

-

- {builderCandidateStep - ? formatModelDisplay(builderCandidateStep) - : getI18nOrFallback( - t, - "previewNextStep", - "Choose provider and model to preview the next step." - )} -

-
- - {builderHasDuplicate && ( - + {builderConnectionId === COMBO_BUILDER_AUTO_CONNECTION && + selectedBuilderConnections.length > 1 ? ( +
+
-
- )} + +
+ {selectedBuilderConnections.map((connection) => { + const checked = builderAllowedConnectionIds.includes(connection.id); + return ( + + ); + })} +
+

+ {getI18nOrFallback( + t, + "builderRestrictAccountsHint", + "Leave empty to use the whole active pool. When selected, round-robin / weighted picks stay within this subset of accounts." + )} +

+
+ ) : null} -
- -
- - -
-
+
+ ) : ( +
+

+ {getI18nOrFallback(t, "builderPreview", "Current step preview")} +

+

+ {builderCandidateStep + ? formatModelDisplay(builderCandidateStep) + : getI18nOrFallback( + t, + "previewNextStep", + "Choose provider and model to preview the next step." + )} +

+
+ + {builderHasDuplicate && ( + + {getI18nOrFallback( + t, + "builderDuplicateExact", + "This exact provider/model/account step is already in the combo." + )} + + )} +
+
+ )} + +
+ +
+ + +
+
)} @@ -3823,6 +3844,58 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
+
+ + + {config.reasoningTransportFallback !== "skip" && ( +

+ {getI18nOrFallback( + t, + "reasoningTransportFallbackDropWarning", + "May lose continuation context or cause tool-call continuations to fail." + )} +

+ )} +
setFormData({ ...formData, preserveEncryptedReasoning: checked })} diff --git a/src/shared/validation/schemas/combo.ts b/src/shared/validation/schemas/combo.ts index 79bb9ca56c..c616e63372 100644 --- a/src/shared/validation/schemas/combo.ts +++ b/src/shared/validation/schemas/combo.ts @@ -186,6 +186,7 @@ export const comboRuntimeConfigSchema = z nestedComboMode: z.enum(["flatten", "execute"]).optional(), trackMetrics: z.boolean().optional(), reasoningTokenBufferEnabled: z.boolean().optional(), + reasoningTransportFallback: z.enum(["skip", "drop"]).optional(), compressionMode: compressionModeSchema.optional(), failoverBeforeRetry: z.boolean().optional(), maxSetRetries: z.coerce.number().int().min(0).max(10).optional(), diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index b200674d32..3ec6e6c6d6 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -1012,6 +1012,8 @@ async function handleChatImplementation( const relayConfig = combo.strategy === "context-relay" ? resolveComboConfig(combo, settings) : null; + const reasoningTransportFallback = + combo.config?.reasoningTransportFallback === "skip" ? "skip" : "drop"; // Per-request Auto-Combo controls (#6023 / #6024 / #6025 / #3470): steer an // `auto` combo on this single request without mutating its stored config. const perRequestAutoControls = resolveRequestAutoControls(request.headers); @@ -1087,6 +1089,7 @@ async function handleChatImplementation( correlationId: reqId, conversationId, modelPinned: (target as any)?.modelPinned ?? false, + reasoningTransportFallback, reasoningDecision, reasoningIntent, reasoningRequestTags: requestRoutingTags.tags, @@ -1300,6 +1303,7 @@ async function handleSingleModelChat( reasoningDecision?: ReasoningRuleDecision | null; reasoningIntent?: ExtractedReasoningIntent | null; reasoningRequestTags?: string[]; + reasoningTransportFallback?: "skip" | "drop"; managedLease?: ManagedLeaseDispatchContext | null; /** * Per-target abort signal from combo.ts's targetTimeoutRunner @@ -1374,6 +1378,8 @@ async function handleSingleModelChat( allowRateLimitedConnection: resolvedTarget?.allowRateLimitedConnection === true, providerId: resolvedTarget?.providerId ?? null, correlationId: runtimeOptions?.correlationId ?? null, + reasoningTransportFallback: + redirectCombo.config?.reasoningTransportFallback === "skip" ? "skip" : "drop", conversationId: runtimeOptions?.conversationId ?? null, managedLease: runtimeOptions.managedLease ?? null, // #7360 follow-up — see the primary handleSingleModel closure above. @@ -1845,6 +1851,7 @@ async function handleSingleModelChat( modelPinned: runtimeOptions?.modelPinned ?? false, routingComboId: runtimeOptions?.routingComboId ?? null, sessionAffinityKey: runtimeOptions.sessionAffinityKey ?? null, + reasoningTransportFallback: runtimeOptions.reasoningTransportFallback ?? "skip", managedLease: runtimeOptions.managedLease ?? null, }, runtimeOptions diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index 866ca28b66..63aa617bab 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -422,6 +422,7 @@ export async function executeChatWithBreaker({ conversationId = null, modelPinned = false, routingComboId = null, + reasoningTransportFallback = "skip", sessionAffinityKey = null, managedLease = null, }: ExecuteChatWithBreakerOptions): Promise { @@ -481,6 +482,7 @@ export async function executeChatWithBreaker({ modelPinned, routingComboId, sessionAffinityKey, + reasoningTransportFallback, managedLease, skipResourcePressureGuard: true, onCredentialsRefreshed: async (newCreds: any) => { diff --git a/tests/unit/chat-route-coverage.test.ts b/tests/unit/chat-route-coverage.test.ts index 0b8d4c8806..ff82a1caa2 100644 --- a/tests/unit/chat-route-coverage.test.ts +++ b/tests/unit/chat-route-coverage.test.ts @@ -316,6 +316,87 @@ test("handleChat keeps protected combo fallback separate from Global Fallback Mo assert.equal(json.choices[0].message.content, "Global fallback answered"); }); +test("handleChat defaults a Combo's incompatible reasoning fallback to drop", async () => { + await seedConnection("deepseek", { apiKey: "sk-deepseek-reasoning-drop" }); + await combosDb.createCombo({ + name: "reasoning-transport-drop", + strategy: "priority", + config: { + maxRetries: 0, + retryDelayMs: 0, + }, + models: ["deepseek/deepseek-v4-flash"], + }); + + let upstreamBody: { input?: unknown } | null = null; + globalThis.fetch = async (_url, init = {}) => { + upstreamBody = JSON.parse(String(init.body)); + return new Response( + JSON.stringify({ + id: "resp_reasoning_drop", + object: "response", + status: "completed", + model: "deepseek-v4-flash", + output: [ + { + id: "msg_reasoning_drop", + type: "message", + role: "assistant", + content: [ + { + type: "output_text", + text: "continued without prior reasoning", + annotations: [], + }, + ], + }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + }; + + const response = await handleChat( + buildRequest({ + url: "http://localhost/v1/responses", + body: { + model: "reasoning-transport-drop", + stream: false, + input: [ + { id: "rs_opaque", type: "reasoning", encrypted_content: "provider-state" }, + { + id: "fc_call", + type: "function_call", + call_id: "call_1", + name: "search", + arguments: "{}", + }, + { type: "function_call_output", call_id: "call_1", output: "done" }, + ], + }, + }) + ); + + assert.equal(response.status, 200); + assert.ok(upstreamBody && Array.isArray(upstreamBody.input)); + const upstreamInput = upstreamBody.input; + assert.equal( + upstreamInput.some( + (item) => + item !== null && typeof item === "object" && "type" in item && item.type === "reasoning" + ), + false + ); + assert.equal( + upstreamInput.some( + (item) => + item !== null && typeof item === "object" && "type" in item && item.type === "function_call" + ), + true + ); +}); + test("handleChat keeps the combo error when the global fallback throws", async () => { await seedConnection("openai", { apiKey: "sk-openai-combo-fail" }); await seedConnection("claude", { apiKey: "sk-claude-fallback-throw" }); diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index 8ae3d4d36d..08cc564cc4 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -212,6 +212,64 @@ function buildResponsesResponse(text = "ok") { ); } +function buildDeepSeekResponsesToolResponse({ + stream, + callId, + reasoning, +}: { + stream: boolean; + callId: string; + reasoning: string; +}) { + const reasoningItem = { + id: "rs_deepseek_tool", + type: "reasoning", + status: "completed", + summary: [], + content: [{ type: "reasoning_text", text: reasoning }], + }; + const functionCall = { + id: "fc_deepseek_tool", + type: "function_call", + status: "completed", + call_id: callId, + name: "inspect", + arguments: "{}", + }; + const response = { + id: "resp_deepseek_tool", + object: "response", + status: "completed", + model: "deepseek-v4-flash", + output: [reasoningItem, functionCall], + usage: { input_tokens: 4, output_tokens: 2, total_tokens: 6 }, + }; + + if (!stream) { + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + const events = [ + { + type: "response.created", + response: { id: response.id, model: response.model, status: "in_progress" }, + }, + { type: "response.output_item.done", output_index: 0, item: reasoningItem }, + { type: "response.output_item.done", output_index: 1, item: functionCall }, + { type: "response.completed", response }, + ]; + return new Response( + `${events.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}`).join("\n\n")}\n\ndata: [DONE]\n\n`, + { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + } + ); +} + function capabilityEntry(limitContext) { return { tool_call: true, @@ -311,6 +369,7 @@ async function invokeChatCore({ onCredentialsRefreshed = null, onRequestSuccess = null, sessionAffinityKey = null, + reasoningTransportFallback = "skip", managedLease = null, cachedSettings = null, }: any = {}) { @@ -359,6 +418,7 @@ async function invokeChatCore({ sessionAffinityKey, isCombo, comboStrategy, + reasoningTransportFallback, managedLease, cachedSettings, onCredentialsRefreshed, @@ -571,7 +631,7 @@ test("chatCore translates a streaming Responses upstream for a Chat client", asy assert.match(streamed, /"content":"ok"/); assert.match(streamed, /data: \[DONE\]/); }); -test("chatCore applies Responses input policy to openai-compatible targets", async () => { +test("chatCore rejects opaque reasoning for unknown Responses targets unless explicitly enabled", async () => { const reasoningItems = [ { id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" }, { type: "reasoning", encrypted_content: "" }, @@ -580,38 +640,300 @@ test("chatCore applies Responses input policy to openai-compatible targets", asy { id: "fc_call", type: "function_call", call_id: "call_1", name: "search", arguments: "{}" }, ]; - for (const preserveEncryptedReasoning of [false, true]) { - const { call, result } = await invokeChatCore({ + const rejected = await invokeChatCore({ + provider: "openai-compatible-sp-openai", + model: "gpt-5.4", + endpoint: "/v1/responses", + credentials: { + apiKey: "sk-test", + providerSpecificData: { + apiType: "responses", + baseUrl: "https://proxy.example.com/v1", + prefix: "sp-openai", + }, + }, + body: { model: "gpt-5.4", stream: false, input: reasoningItems }, + responseFormat: "openai-responses", + }); + + assert.equal(rejected.result.success, false); + assert.equal(rejected.result.status, 400); + assert.equal(rejected.calls.length, 0); + + const enabled = await invokeChatCore({ + provider: "openai-compatible-sp-openai", + model: "gpt-5.4", + endpoint: "/v1/responses", + credentials: { + apiKey: "sk-test", + providerSpecificData: { + apiType: "responses", + baseUrl: "https://proxy.example.com/v1", + prefix: "sp-openai", + preserveEncryptedReasoning: true, + }, + }, + body: { model: "gpt-5.4", stream: false, input: reasoningItems }, + responseFormat: "openai-responses", + }); + + assert.equal(enabled.result.success, true); + const input = enabled.call.body.input as Array>; + assert.deepEqual( + input.filter((item) => item.type === "reasoning"), + [ + { id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" }, + { type: "reasoning", summary: [{ text: "not self-contained" }] }, + ] + ); + assert.equal( + input.some((item) => item.type === "item_reference"), + false + ); + assert.equal(input.find((item) => item.type === "function_call")?.id, undefined); +}); + +test("chatCore applies Chat reasoning compatibility before stream mode diverges", async () => { + for (const stream of [false, true]) { + const rejected = await invokeChatCore({ provider: "openai-compatible-sp-openai", model: "gpt-5.4", - endpoint: "/v1/responses", + endpoint: "/v1/chat/completions", credentials: { apiKey: "sk-test", providerSpecificData: { - apiType: "responses", + apiType: "openai", baseUrl: "https://proxy.example.com/v1", prefix: "sp-openai", - preserveEncryptedReasoning, }, }, - body: { model: "gpt-5.4", stream: false, input: reasoningItems }, - responseFormat: "openai-responses", + body: { + model: "gpt-5.4", + stream, + messages: [ + { + role: "assistant", + content: null, + reasoning_details: [{ type: "reasoning.encrypted", data: "provider-state" }], + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "search", arguments: "{}" }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", content: "result" }, + ], + }, }); - assert.equal(result.success, true); - const input = call.body.input as Array>; - assert.deepEqual( - input.filter((item) => item.type === "reasoning"), - preserveEncryptedReasoning ? [{ type: "reasoning", encrypted_content: "encrypted-blob" }] : [] - ); - assert.equal( - input.some((item) => item.type === "item_reference"), - false - ); - assert.equal(input.find((item) => item.type === "function_call")?.id, undefined); + assert.equal(rejected.result.success, false, `stream=${stream}`); + assert.equal(rejected.result.status, 400, `stream=${stream}`); + assert.equal(rejected.calls.length, 0, `stream=${stream}`); } }); +test("chatCore can drop incompatible reasoning for an opted-in Combo attempt", async () => { + const dropped = await invokeChatCore({ + provider: "openai-compatible-sp-openai", + model: "gpt-5.4", + endpoint: "/v1/responses", + credentials: { + apiKey: "sk-test", + providerSpecificData: { + apiType: "responses", + baseUrl: "https://proxy.example.com/v1", + prefix: "sp-openai", + }, + }, + body: { + model: "gpt-5.4", + stream: false, + input: [ + { id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" }, + { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, + ], + }, + responseFormat: "openai-responses", + isCombo: true, + reasoningTransportFallback: "drop", + }); + + assert.equal(dropped.result.success, true); + assert.equal(dropped.calls.length, 1); + assert.equal( + dropped.call.body.input.some((item) => item.type === "reasoning"), + false + ); +}); + +test("chatCore carries Chat reasoning_content into official DeepSeek Responses input", async () => { + const { call, result } = await invokeChatCore({ + provider: "deepseek", + model: "deepseek-v4-pro", + endpoint: "/v1/chat/completions", + body: { + model: "deepseek-v4-pro", + stream: false, + messages: [ + { + role: "assistant", + content: null, + reasoning_content: "Inspect before calling the tool", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "search", arguments: "{}" }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", content: "found" }, + ], + }, + responseFormat: "openai-responses", + }); + + assert.equal(result.success, true); + assert.match(call.url, /\/responses$/); + assert.deepEqual(call.body.input.slice(0, 3), [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "Inspect before calling the tool" }], + }, + { + type: "function_call", + call_id: "call_1", + name: "search", + arguments: "{}", + status: "completed", + }, + { type: "function_call_output", call_id: "call_1", output: "found", status: "completed" }, + ]); +}); + +test("chatCore replays nonstream DeepSeek Responses reasoning across a Chat tool turn", async () => { + const callId = "call_deepseek_nonstream_replay"; + const reasoning = "Authentic nonstream DeepSeek reasoning"; + const apiKeyInfo = { id: "deepseek-nonstream-chat-key" }; + const first = await invokeChatCore({ + provider: "deepseek", + model: "deepseek-v4-flash", + endpoint: "/v1/chat/completions", + body: { + model: "deepseek-v4-flash", + stream: false, + reasoning_effort: "high", + messages: [{ role: "user", content: "Inspect the repository" }], + tools: [ + { + type: "function", + function: { name: "inspect", description: "Inspect", parameters: { type: "object" } }, + }, + ], + }, + apiKeyInfo, + responseFactory: () => buildDeepSeekResponsesToolResponse({ stream: false, callId, reasoning }), + }); + + assert.equal(first.result.success, true); + const firstPayload = (await first.result.response.json()) as { + choices: Array<{ message: Record & { reasoning_content?: string } }>; + }; + assert.equal(firstPayload.choices[0].message.reasoning_content, reasoning); + const assistant = structuredClone(firstPayload.choices[0].message); + delete assistant.reasoning_content; + + const second = await invokeChatCore({ + provider: "deepseek", + model: "deepseek-v4-flash", + endpoint: "/v1/chat/completions", + body: { + model: "deepseek-v4-flash", + stream: false, + reasoning_effort: "high", + messages: [ + { role: "user", content: "Inspect the repository" }, + assistant, + { role: "tool", tool_call_id: callId, content: "inspection complete" }, + ], + }, + apiKeyInfo, + responseFactory: () => buildResponsesResponse("done"), + }); + + assert.equal(second.result.success, true); + assert.deepEqual( + second.call.body.input.find((item) => item.type === "reasoning"), + { type: "reasoning", content: [{ type: "reasoning_text", text: reasoning }] } + ); +}); + +test("chatCore replays streamed DeepSeek Responses reasoning across a Chat tool turn", async () => { + const callId = "call_deepseek_stream_replay"; + const reasoning = "Authentic streamed DeepSeek reasoning"; + const apiKeyInfo = { id: "deepseek-stream-chat-key" }; + const first = await invokeChatCore({ + provider: "deepseek", + model: "deepseek-v4-flash", + endpoint: "/v1/chat/completions", + body: { + model: "deepseek-v4-flash", + stream: true, + reasoning_effort: "high", + messages: [{ role: "user", content: "Inspect the repository" }], + tools: [ + { + type: "function", + function: { name: "inspect", description: "Inspect", parameters: { type: "object" } }, + }, + ], + }, + apiKeyInfo, + responseFactory: () => buildDeepSeekResponsesToolResponse({ stream: true, callId, reasoning }), + }); + + assert.equal(first.result.success, true); + const streamed = await first.result.response.text(); + assert.match(streamed, new RegExp(reasoning)); + await flushAsyncSideEffects(); + + const second = await invokeChatCore({ + provider: "deepseek", + model: "deepseek-v4-flash", + endpoint: "/v1/chat/completions", + body: { + model: "deepseek-v4-flash", + stream: false, + reasoning_effort: "high", + messages: [ + { role: "user", content: "Inspect the repository" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: callId, + type: "function", + function: { name: "inspect", arguments: "{}" }, + }, + ], + }, + { role: "tool", tool_call_id: callId, content: "inspection complete" }, + ], + }, + apiKeyInfo, + responseFactory: () => buildResponsesResponse("done"), + }); + + assert.equal(second.result.success, true); + assert.deepEqual( + second.call.body.input.find((item) => item.type === "reasoning"), + { type: "reasoning", content: [{ type: "reasoning_text", text: reasoning }] } + ); +}); + test("chatCore replays no-tool reasoning across public Responses turns", async () => { // Direct DeepSeek now speaks Responses upstream. Keep this regression on a // Chat-compatible DeepSeek host so it continues to exercise the Responses-to-Chat replay path. @@ -785,14 +1107,14 @@ test("chatCore captures streaming no-tool reasoning for Responses replay", async assert.equal(second.result.success, true); assert.equal(second.call.body.messages[1].reasoning_content, "Authentic streaming reasoning"); }); -test("chatCore preserves opted-in encrypted reasoning for Codex", async () => { +test("chatCore automatically preserves provider-generated opaque reasoning for Codex", async () => { const { call, result } = await invokeChatCore({ provider: "codex", model: "gpt-5.1-codex", endpoint: "/v1/responses", credentials: { accessToken: "codex-token", - providerSpecificData: { preserveEncryptedReasoning: true }, + providerSpecificData: {}, }, body: { model: "gpt-5.1-codex", @@ -810,7 +1132,7 @@ test("chatCore preserves opted-in encrypted reasoning for Codex", async () => { assert.equal(result.success, true); assert.deepEqual( call.body.input.filter((item) => item.type === "reasoning"), - [{ type: "reasoning", encrypted_content: "encrypted-blob" }] + [{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" }] ); assert.equal( call.body.input.some((item) => item.type === "item_reference"), diff --git a/tests/unit/combo-attempt-body-isolation-7847.test.ts b/tests/unit/combo-attempt-body-isolation-7847.test.ts index 6238baf8ff..9373c51e7c 100644 --- a/tests/unit/combo-attempt-body-isolation-7847.test.ts +++ b/tests/unit/combo-attempt-body-isolation-7847.test.ts @@ -22,6 +22,8 @@ const ORIGINAL_DATA_DIR = process.env.DATA_DIR; process.env.DATA_DIR = TEST_DATA_DIR; const { handleComboChat } = await import("../../open-sse/services/combo.ts"); +const { applyReasoningInputPolicy } = + await import("../../open-sse/services/reasoningInputPolicy.ts"); const core = await import("../../src/lib/db/core.ts"); const { resetAllComboMetrics } = await import("../../open-sse/services/comboMetrics.ts"); const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); @@ -164,6 +166,85 @@ test("the caller's body object is never mutated by the combo loop", async () => assert.equal(JSON.stringify(body), before, "handleComboChat must treat `body` as read-only"); }); +test("incompatible reasoning skips a target without mutating the fallback attempt", async () => { + const body = { + model: "deepseek/deepseek-v4-pro", + input: [ + { + id: "rs_plaintext", + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + }, + { + id: "fc_shared", + type: "function_call", + call_id: "call_1", + name: "search", + arguments: "{}", + }, + ], + }; + let attempts = 0; + + const response = await handleComboChat({ + body, + combo: comboOf("priority", "reasoning-policy-isolation"), + handleSingleModel: async (received: Record) => { + attempts++; + const input = received.input as Array>; + if (attempts === 1) { + const policy = applyReasoningInputPolicy(received, "responses", { + provider: "openai", + onIncompatibleReasoning: "drop", + }); + assert.equal(policy.incompatibleReasoning, false); + assert.equal( + (received.input as Array>).some( + (item) => item.type === "reasoning" + ), + false + ); + return new Response( + JSON.stringify({ + error: { + message: "Reasoning continuation is not compatible with the selected target", + }, + }), + { status: 400, headers: { "content-type": "application/json" } } + ); + } + + assert.equal( + input[1].id, + "fc_shared", + "the first target's nested input rewrite leaked into the fallback" + ); + const policy = applyReasoningInputPolicy(received, "responses", { + provider: "deepseek", + }); + assert.equal(policy.incompatibleReasoning, false); + assert.equal( + (received.input as Array>)[0].type, + "reasoning", + "the compatible fallback lost the plaintext reasoning item" + ); + return okResponse(); + }, + isModelAvailable: async () => true, + log: createLog(), + settings: null, + allCombos: null, + }); + + assert.equal(response.status, 200); + assert.ok(attempts >= 2); + assert.equal( + (body.input[1] as Record).id, + "fc_shared", + "the caller's nested input was mutated" + ); +}); + // ── The copy must stay shallow — that is the whole point ───────────────────── test("the per-target copy shares the nested payload instead of deep-cloning it", async () => { const body = agentBody(); diff --git a/tests/unit/combo-config.test.ts b/tests/unit/combo-config.test.ts index 61fe705d0a..d09ecf6999 100644 --- a/tests/unit/combo-config.test.ts +++ b/tests/unit/combo-config.test.ts @@ -647,6 +647,24 @@ test("createComboSchema accepts nestedComboMode and rejects invalid values", () assert.equal(invalid.success, false); }); +test("createComboSchema validates reasoning transport fallback modes", () => { + for (const mode of ["skip", "drop"] as const) { + const parsed = createComboSchema.parse({ + name: `reasoning-transport-${mode}`, + models: ["openai/gpt-5.4"], + config: { reasoningTransportFallback: mode }, + }); + assert.equal(parsed.config.reasoningTransportFallback, mode); + } + + const invalid = createComboSchema.safeParse({ + name: "reasoning-transport-invalid", + models: ["openai/gpt-5.4"], + config: { reasoningTransportFallback: "retry" }, + }); + assert.equal(invalid.success, false); +}); + test("createComboSchema accepts per-combo stickyRoundRobinLimit and rejects out-of-range", () => { const parsed = createComboSchema.parse({ name: "sticky-override", diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 9913e02b5c..359768a3bd 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -450,7 +450,7 @@ test("CodexExecutor.transformRequest strips store from compact requests even whe assert.equal(result.instructions, "keep this"); }); -test("CodexExecutor.transformRequest preserves native assistant commentary history", () => { +test("CodexExecutor.transformRequest preserves commentary and strips orphan summaries", () => { const executor = new CodexExecutor(); const body = { _nativeCodexPassthrough: true, @@ -526,9 +526,8 @@ test("CodexExecutor.transformRequest preserves native assistant commentary histo ), true ); - // Reasoning items are stripped from the Responses input — encrypted_content is - // unusable with store=false (previous_response_id deleted) and the summary blob - // only inflates context on every subsequent agentic turn (decolua/9router#1599). + // Summary-only reasoning is display state, not continuation state. Replaying it + // with store=false only inflates every subsequent agentic turn (decolua/9router#1599). assert.equal( result.input.some((item) => item.type === "reasoning"), false @@ -543,6 +542,54 @@ test("CodexExecutor.transformRequest preserves native assistant commentary histo ); }); +test("CodexExecutor.transformRequest preserves active opaque reasoning with a summary", () => { + const executor = new CodexExecutor(); + const reasoning = { + type: "reasoning", + encrypted_content: "provider-state", + summary: [{ type: "summary_text", text: "Display summary" }], + }; + + const result = executor.transformRequest( + "gpt-5.5-low", + { + _nativeCodexPassthrough: true, + input: [reasoning], + stream: false, + }, + false, + { requestEndpointPath: "/responses" } + ); + + assert.equal(result.store, false); + assert.deepEqual(result.input, [reasoning]); +}); + +test("CodexExecutor.transformRequest preserves orphan summaries when store is enabled", () => { + const executor = new CodexExecutor(); + const reasoning = { + type: "reasoning", + summary: [{ type: "summary_text", text: "Display summary" }], + }; + + const result = executor.transformRequest( + "gpt-5.5-low", + { + _nativeCodexPassthrough: true, + input: [reasoning], + stream: false, + }, + false, + { + requestEndpointPath: "/responses", + providerSpecificData: { openaiStoreEnabled: true }, + } + ); + + assert.equal(result.store, true); + assert.deepEqual(result.input, [reasoning]); +}); + test("CodexExecutor.transformRequest still strips assistant commentary outside native passthrough", () => { const executor = new CodexExecutor(); const result = executor.transformRequest( diff --git a/tests/unit/kimi-coding-translator.test.ts b/tests/unit/kimi-coding-translator.test.ts index f1ec07421b..42cd017663 100644 --- a/tests/unit/kimi-coding-translator.test.ts +++ b/tests/unit/kimi-coding-translator.test.ts @@ -97,7 +97,7 @@ test("Responses history preserves Kimi reasoning before a tool call", () => { { id: "rs_1", type: "reasoning", - summary: [{ type: "summary_text", text: "I should call ping first." }], + content: [{ type: "reasoning_text", text: "I should call ping first." }], }, { id: "fc_1", @@ -147,7 +147,7 @@ test("Responses history preserves Kimi reasoning on completed assistant turns", { id: "rs_1", type: "reasoning", - summary: [{ type: "summary_text", text: "I should retain the nonce." }], + content: [{ type: "reasoning_text", text: "I should retain the nonce." }], }, { type: "message", @@ -182,7 +182,7 @@ test("Responses history preserves Kimi reasoning before a custom tool call", () input: [ { type: "reasoning", - summary: [{ type: "summary_text", text: "I should apply the patch." }], + content: [{ type: "reasoning_text", text: "I should apply the patch." }], }, { type: "custom_tool_call", @@ -223,7 +223,7 @@ test("Responses history does not carry reasoning across a user boundary", () => }, { type: "reasoning", - summary: [{ type: "summary_text", text: "Prior turn reasoning." }], + content: [{ type: "reasoning_text", text: "Prior turn reasoning." }], }, { role: "user", diff --git a/tests/unit/moonshot-k3.test.ts b/tests/unit/moonshot-k3.test.ts index d85b67855b..ec77244240 100644 --- a/tests/unit/moonshot-k3.test.ts +++ b/tests/unit/moonshot-k3.test.ts @@ -202,7 +202,7 @@ test("Responses history preserves authentic reasoning for native Moonshot K3", ( }, { type: "reasoning", - summary: [{ type: "summary_text", text: "I should search first." }], + content: [{ type: "reasoning_text", text: "I should search first." }], }, { type: "function_call", diff --git a/tests/unit/reasoning-cache.test.ts b/tests/unit/reasoning-cache.test.ts index 9c144596b4..e13dfec7a9 100644 --- a/tests/unit/reasoning-cache.test.ts +++ b/tests/unit/reasoning-cache.test.ts @@ -36,6 +36,7 @@ import { import { translateRequest } from "../../open-sse/translator/index.ts"; import { FORMATS } from "../../open-sse/translator/formats.ts"; import { ensureToolCallIds } from "../../open-sse/translator/helpers/toolCallHelper.ts"; +import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts"; import { getDbInstance } from "../../src/lib/db/core.ts"; import { getReasoningCache, setReasoningCache } from "../../src/lib/db/reasoningCache.ts"; import { DELETE, GET } from "../../src/app/api/cache/reasoning/route.ts"; @@ -625,6 +626,48 @@ describe("Reasoning Replay Cache — Translator Replay", () => { assert.equal(getReasoningCacheServiceStats().replays, 1); }); + it("should replay cached DeepSeek reasoning before Chat converts to Responses input", () => { + clearReasoningCacheAll(); + clearModelsDevCapabilities(); + const callId = "call_ds_chat_to_responses"; + cacheReasoning(callId, "deepseek", "deepseek-v4-flash", "Cached Chat continuation reasoning"); + + const translated = translateRequest( + FORMATS.OPENAI, + FORMATS.OPENAI_RESPONSES, + "deepseek-v4-flash", + { + reasoning_effort: "high", + messages: [ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: callId, + type: "function", + function: { name: "read_file", arguments: "{}" }, + }, + ], + }, + { role: "tool", tool_call_id: callId, content: "contents" }, + ], + }, + false, + null, + "deepseek" + ); + + assert.deepEqual( + translated.input.find((item) => item.type === "reasoning"), + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "Cached Chat continuation reasoning" }], + } + ); + }); + it("should preserve DeepSeek Responses reasoning before Chat conversion", () => { clearReasoningCacheAll(); clearModelsDevCapabilities(); @@ -645,7 +688,7 @@ describe("Reasoning Replay Cache — Translator Replay", () => { input: [ { type: "reasoning", - summary: [{ type: "summary_text", text: "Client DeepSeek reasoning" }], + content: [{ type: "reasoning_text", text: "Client DeepSeek reasoning" }], }, { type: "message", @@ -681,6 +724,76 @@ describe("Reasoning Replay Cache — Translator Replay", () => { assert.equal(statsAfterTranslation.replays, statsBeforeTranslation.replays); }); + it("should cache only authentic plaintext from nonstream Responses output", () => { + clearReasoningCacheAll(); + const callId = "call_nonstream_authentic_reasoning"; + const translated = translateNonStreamingResponse( + { + object: "response", + model: "deepseek-v4-flash", + output: [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "Authentic provider reasoning" }], + summary: [{ type: "summary_text", text: "Display summary" }], + }, + { type: "function_call", call_id: callId, name: "read_file", arguments: "{}" }, + ], + }, + FORMATS.OPENAI_RESPONSES, + FORMATS.OPENAI + ) as { choices?: Array<{ message?: Record }> }; + const message = translated.choices?.[0]?.message; + + assert.ok(message); + assert.equal(message.reasoning_content, "Authentic provider reasoning"); + assert.equal(cacheReasoningFromAssistantMessage(message, "deepseek", "deepseek-v4-flash"), 1); + assert.equal(lookupReasoning(callId), "Authentic provider reasoning"); + }); + + it("should never cache Responses summaries or opaque plaintext companions", () => { + for (const [suffix, reasoningItem] of [ + [ + "summary", + { + type: "reasoning", + summary: [{ type: "summary_text", text: "Display-only summary" }], + }, + ], + [ + "mixed", + { + type: "reasoning", + encrypted_content: "opaque-provider-state", + content: [{ type: "reasoning_text", text: "Unsafe plaintext companion" }], + summary: [{ type: "summary_text", text: "Display-only mixed summary" }], + }, + ], + ] as const) { + clearReasoningCacheAll(); + const callId = `call_nonstream_${suffix}_reasoning`; + const translated = translateNonStreamingResponse( + { + object: "response", + model: "deepseek-v4-flash", + output: [ + reasoningItem, + { type: "function_call", call_id: callId, name: "read_file", arguments: "{}" }, + ], + }, + FORMATS.OPENAI_RESPONSES, + FORMATS.OPENAI + ) as { choices?: Array<{ message?: Record }> }; + const message = translated.choices?.[0]?.message; + + assert.ok(message); + assert.equal(message.reasoning_content, undefined); + assert.ok(Array.isArray(message.reasoning_summary)); + assert.equal(cacheReasoningFromAssistantMessage(message, "deepseek", "deepseek-v4-flash"), 0); + assert.equal(lookupReasoning(callId), null); + } + }); + it("should preserve client-provided reasoning content", () => { clearReasoningCacheAll(); clearModelsDevCapabilities(); diff --git a/tests/unit/response-sanitizer.test.ts b/tests/unit/response-sanitizer.test.ts index 7fecd106fd..925b9c3795 100644 --- a/tests/unit/response-sanitizer.test.ts +++ b/tests/unit/response-sanitizer.test.ts @@ -423,6 +423,32 @@ test("sanitizeResponsesApiResponse preserves native Responses payloads and usage assert.equal((sanitized as any).usage.output_tokens_details.reasoning_tokens, 3); }); +test("sanitizeResponsesApiResponse preserves native continuation reasoning state", () => { + const plaintext = { + id: "rs_plaintext", + type: "reasoning", + status: "completed", + summary: [], + content: [{ type: "reasoning_text", text: "Inspect before calling the tool." }], + }; + const opaque = { + id: "rs_opaque", + type: "reasoning", + summary: [{ type: "summary_text", text: "Inspected the tool inputs." }], + encrypted_content: "provider-generated-state", + signature: "provider-signature", + format: "provider-format", + }; + const sanitized = sanitizeResponsesApiResponse({ + id: "resp_reasoning_state", + object: "response", + status: "completed", + output: [plaintext, opaque], + }) as Record; + + assert.deepEqual(sanitized.output, [plaintext, opaque]); +}); + test("sanitizeStreamingChunk keeps only safe chunk fields and preserves readable reasoning aliases", () => { const sanitized = sanitizeStreamingChunk({ id: "chunk_1", diff --git a/tests/unit/responses-handler.test.ts b/tests/unit/responses-handler.test.ts index 0ec6adf448..7afbe12d31 100644 --- a/tests/unit/responses-handler.test.ts +++ b/tests/unit/responses-handler.test.ts @@ -233,7 +233,7 @@ test("handleResponsesCore preserves Kimi K3 reasoning through provider translati { role: "user", content: [{ type: "input_text", text: "Call search." }] }, { type: "reasoning", - summary: [{ type: "summary_text", text: "I should search first." }], + content: [{ type: "reasoning_text", text: "I should search first." }], }, { type: "function_call", diff --git a/tests/unit/responses-translation-fixes.test.ts b/tests/unit/responses-translation-fixes.test.ts index 1f3fec359a..66807e11ec 100644 --- a/tests/unit/responses-translation-fixes.test.ts +++ b/tests/unit/responses-translation-fixes.test.ts @@ -45,7 +45,7 @@ test("production Responses conversion preserves Kimi K3 reasoning history", () = { role: "user", content: [{ type: "input_text", text: "Call search." }] }, { type: "reasoning", - summary: [{ type: "summary_text", text: "I should search first." }], + content: [{ type: "reasoning_text", text: "I should search first." }], }, { type: "function_call", @@ -87,7 +87,7 @@ test("Responses translation keeps authentic K3 reasoning through OpenAI cleanup" { role: "user", content: [{ type: "input_text", text: "Call search." }] }, { type: "reasoning", - summary: [{ type: "summary_text", text: "I should search first." }], + content: [{ type: "reasoning_text", text: "I should search first." }], }, { type: "function_call", diff --git a/tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts b/tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts index 246c0668db..cab5e9c37f 100644 --- a/tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts +++ b/tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts @@ -1,18 +1,18 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { applyResponsesInputPolicy } from "../../open-sse/services/responsesInputPolicy.ts"; +import { applyReasoningInputPolicy } from "../../open-sse/services/reasoningInputPolicy.ts"; import { filterToOpenAIFormat } from "../../open-sse/translator/helpers/openaiHelper.ts"; +import { omitEncryptedReasoningForLog } from "../../src/lib/logPayloads.ts"; -// Port of decolua/9router#1599 — strip unusable reasoning blobs from agentic -// context to prevent O(n^2) token growth across turns. Encrypted reasoning is -// self-contained and may be replayed only through an explicit connection opt-in. +// Responses reasoning replay is target-scoped. Plaintext DeepSeek state and +// provider-generated opaque state are never interchangeable. -test("applyResponsesInputPolicy drops object items with type=reasoning", () => { +test("unknown Responses targets reject opaque reasoning and ignore display summaries", () => { const body: Record = { input: [ { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, - { id: "rs_abc123", type: "reasoning", summary: [{ text: "thinking..." }] }, + { id: "rs_abc123", type: "reasoning", summary: [{ text: "display only" }] }, { type: "reasoning", encrypted_content: "blob" }, { type: "function_call", @@ -24,82 +24,315 @@ test("applyResponsesInputPolicy drops object items with type=reasoning", () => { ], }; - applyResponsesInputPolicy(body); + const originalInput = structuredClone(body.input); + const result = applyReasoningInputPolicy(body, "responses"); - const input = body.input as Array>; - // Both reasoning items must be gone. - assert.equal( - input.some((it) => it && it.type === "reasoning"), - false, - "reasoning items must be stripped" - ); - // Non-reasoning items survive (message + function_call), id is sanitized. - assert.equal(input.length, 2); - assert.equal(input[0].type, "message"); - assert.equal(input[1].type, "function_call"); - assert.equal(input[1].id, undefined, "fc_ server id stripped, item kept"); + assert.equal(result.incompatibleReasoning, true); + assert.deepEqual(body.input, originalInput, "rejection must not mutate the request"); }); -test("selected connection policy preserves encrypted reasoning input", () => { +test("unannotated targets preserve plaintext Responses reasoning without synthetic IDs", () => { const body: Record = { input: [ { - id: "rs_encrypted123", + id: "rs_plaintext123", type: "reasoning", - encrypted_content: "encrypted-blob", - summary: [{ type: "summary_text", text: "safe summary" }], + content: [{ type: "reasoning_text", text: "inspect first" }], }, { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, ], }; - applyResponsesInputPolicy(body, true); + const result = applyReasoningInputPolicy(body, "responses", { provider: "opencode-go" }); + assert.equal(result.incompatibleReasoning, false); assert.deepEqual(body.input, [ { type: "reasoning", - encrypted_content: "encrypted-blob", - summary: [{ type: "summary_text", text: "safe summary" }], + content: [{ type: "reasoning_text", text: "inspect first" }], }, { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, ]); }); -test("preserving encrypted reasoning still removes stored references", () => { +test("display summaries coexist with plaintext continuation without affecting compatibility", () => { const body: Record = { input: [ - { id: "rs_encrypted123", type: "reasoning", encrypted_content: "encrypted-blob" }, + { + id: "rs_plaintext_summary", + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + summary: [{ type: "summary_text", text: "display only" }], + }, + ], + }; + + const result = applyReasoningInputPolicy(body, "responses", { provider: "opencode-go" }); + + assert.equal(result.incompatibleReasoning, false); + assert.deepEqual(body.input, [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + summary: [{ type: "summary_text", text: "display only" }], + }, + ]); +}); + +test("unannotated targets preserve Chat plaintext and ignore display summaries", () => { + const body: Record = { + messages: [ + { + role: "assistant", + content: null, + reasoning_content: "inspect first", + summary_text: "display only", + }, + { role: "user", content: "continue" }, + ], + }; + const originalMessages = body.messages; + + const result = applyReasoningInputPolicy(body, "chat", { provider: "opencode-go" }); + + assert.equal(result.incompatibleReasoning, false); + assert.equal(body.messages, originalMessages, "compatible Chat history should not be cloned"); +}); + +test("Chat drop removes opaque state while preserving plaintext and summary details", () => { + const body: Record = { + messages: [ + { + role: "assistant", + content: null, + reasoning_content: "inspect first", + reasoning_details: [ + { type: "reasoning.encrypted", data: "provider-state" }, + { type: "reasoning.summary", text: "display only" }, + ], + }, + { role: "user", content: "continue" }, + ], + }; + + const result = applyReasoningInputPolicy(body, "chat", { + provider: "opencode-go", + onIncompatibleReasoning: "drop", + }); + + assert.equal(result.incompatibleReasoning, false); + assert.deepEqual(body.messages, [ + { + role: "assistant", + content: null, + reasoning_content: "inspect first", + reasoning_details: [{ type: "reasoning.summary", text: "display only" }], + }, + { role: "user", content: "continue" }, + ]); +}); + +test("DeepSeek rejects plaintext reasoning carrying opaque provider state", () => { + for (const opaqueField of ["signature", "format"] as const) { + const body: Record = { + input: [ + { + id: "rs_mixed123", + type: "reasoning", + content: [{ type: "reasoning_text", text: "untrusted companion" }], + [opaqueField]: "provider-state", + }, + { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, + ], + }; + + const result = applyReasoningInputPolicy(body, "responses", { provider: "deepseek" }); + + assert.equal(result.incompatibleReasoning, true, opaqueField); + assert.deepEqual(body.input, [ + { + id: "rs_mixed123", + type: "reasoning", + content: [{ type: "reasoning_text", text: "untrusted companion" }], + [opaqueField]: "provider-state", + }, + { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, + ]); + } +}); + +test("drop fallback removes only the incompatible active transport and preserves summaries", () => { + const plaintextReasoning = { + id: "rs_plaintext", + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + }; + const opaqueReasoning = { + id: "rs_opaque", + type: "reasoning", + encrypted_content: "provider-state", + summary: [{ type: "summary_text", text: "display only" }], + }; + const body: Record = { + input: [ + plaintextReasoning, + opaqueReasoning, + { id: "fc_call", type: "function_call", call_id: "call_1", name: "search" }, + ], + }; + + const result = applyReasoningInputPolicy(body, "responses", { + provider: "deepseek", + onIncompatibleReasoning: "drop", + }); + + assert.equal(result.incompatibleReasoning, false); + assert.deepEqual(body.input, [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + }, + { type: "reasoning", summary: [{ type: "summary_text", text: "display only" }] }, + { type: "function_call", call_id: "call_1", name: "search" }, + ]); + assert.equal(plaintextReasoning.id, "rs_plaintext"); + assert.equal(opaqueReasoning.encrypted_content, "provider-state"); +}); + +test("drop fallback preserves reasoning when its transport is compatible", () => { + const body: Record = { + input: [ + { + id: "rs_plaintext", + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + }, + ], + }; + + const result = applyReasoningInputPolicy(body, "responses", { + provider: "deepseek", + onIncompatibleReasoning: "drop", + }); + + assert.equal(result.incompatibleReasoning, false); + assert.deepEqual(body.input, [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "inspect first" }], + }, + ]); +}); + +test("known opaque targets preserve a cloned complete provider-generated reasoning item", () => { + const opaqueReasoning = { + id: "rs_encrypted123", + type: "reasoning", + encrypted_content: "encrypted-blob", + summary: [{ type: "summary_text", text: "safe summary" }], + status: "completed", + }; + const body: Record = { + input: [ + opaqueReasoning, "rs_stored123", { type: "item_reference", id: "resp_stored123" }, + "ws_stored123", + { type: "web_search_call", id: "ws_stored123", status: "completed" }, + { type: "image_generation_call", id: "ig_stored123", status: "completed" }, { type: "function_call", id: "fc_stored123", call_id: "call_1" }, ], }; - applyResponsesInputPolicy(body, true); + const result = applyReasoningInputPolicy(body, "responses", { provider: "openai" }); + assert.notEqual( + (body.input as Array>)[0], + opaqueReasoning, + "policy output must not expose the caller's nested item object" + ); + assert.equal(result.incompatibleReasoning, false); assert.deepEqual(body.input, [ - { type: "reasoning", encrypted_content: "encrypted-blob" }, + { + id: "rs_encrypted123", + type: "reasoning", + encrypted_content: "encrypted-blob", + summary: [{ type: "summary_text", text: "safe summary" }], + status: "completed", + }, + { type: "web_search_call", status: "completed" }, + { type: "image_generation_call", status: "completed" }, { type: "function_call", call_id: "call_1" }, ]); }); -test("applyResponsesInputPolicy still drops summary-only reasoning when enabled", () => { +test("explicit custom target opt-in remains an opaque transport override", () => { + const body: Record = { + input: [{ type: "reasoning", encrypted_content: "encrypted-blob" }], + }; + + const result = applyReasoningInputPolicy(body, "responses", { preserveEncryptedReasoning: true }); + + assert.equal(result.incompatibleReasoning, false); + assert.deepEqual(body.input, [{ type: "reasoning", encrypted_content: "encrypted-blob" }]); +}); + +test("preserved opaque reasoning remains redacted from log copies", () => { + const body = { + input: [{ type: "reasoning", encrypted_content: "provider-secret-blob" }], + }; + + applyReasoningInputPolicy(body, "responses", { provider: "xai" }); + const logged = omitEncryptedReasoningForLog(body) as typeof body; + + assert.equal(body.input[0].encrypted_content, "provider-secret-blob"); + assert.equal(logged.input[0].encrypted_content, "[omitted: encrypted reasoning, 20 chars]"); +}); + +test("summary-only reasoning is preserved independently of active transport", () => { const body: Record = { input: [ { id: "rs_summary123", type: "reasoning", summary: [{ text: "thinking..." }] }, { type: "reasoning", encrypted_content: "" }, - { type: "reasoning", encrypted_content: 42 }, { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, ], }; - applyResponsesInputPolicy(body, true); + const result = applyReasoningInputPolicy(body, "responses", { provider: "openai" }); + assert.equal(result.incompatibleReasoning, false); assert.deepEqual(body.input, [ + { type: "reasoning", summary: [{ text: "thinking..." }] }, { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, ]); }); +test("stateless Responses input drops orphan summaries but preserves active state", () => { + const activeReasoning = { + type: "reasoning", + encrypted_content: "provider-state", + summary: [{ type: "summary_text", text: "Display summary" }], + }; + const message = { + type: "message", + role: "user", + content: [{ type: "input_text", text: "continue" }], + }; + const body: Record = { + store: false, + input: [ + { type: "reasoning", summary: [{ type: "summary_text", text: "Orphan summary" }] }, + activeReasoning, + message, + ], + }; + + const result = applyReasoningInputPolicy(body, "responses", { provider: "codex" }); + + assert.equal(result.incompatibleReasoning, false); + assert.deepEqual(body.input, [activeReasoning, message]); +}); + test("filterToOpenAIFormat strips reasoning_content from assistant+tool_calls messages", () => { const body = { messages: [ diff --git a/tests/unit/translator-openai-responses-req.test.ts b/tests/unit/translator-openai-responses-req.test.ts index 3aa5178a06..e0d0a51773 100644 --- a/tests/unit/translator-openai-responses-req.test.ts +++ b/tests/unit/translator-openai-responses-req.test.ts @@ -92,7 +92,7 @@ test("Responses -> Chat keeps assistant text, reasoning, and function calls in o input: [ { type: "reasoning", - summary: [{ type: "summary_text", text: "Inspect first" }], + content: [{ type: "reasoning_text", text: "Inspect first" }], }, { type: "message", @@ -133,6 +133,69 @@ test("Responses -> Chat keeps assistant text, reasoning, and function calls in o }); }); +test("Responses -> Chat replays plaintext reasoning_text instead of a display summary", () => { + const result = openaiResponsesToOpenAIRequest( + "deepseek-v4-pro", + { + input: [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "Use the indexed result" }], + summary: [{ type: "summary_text", text: "Display summary" }], + }, + { type: "function_call", call_id: "call_1", name: "search", arguments: "{}" }, + ], + }, + false, + { _preserveReasoningContent: true } + ) as { messages: Array> }; + + assert.equal(result.messages[0].reasoning_content, "Use the indexed result"); +}); + +test("Responses -> Chat keeps summary-only reasoning out of continuation state", () => { + const result = openaiResponsesToOpenAIRequest( + "deepseek-v4-pro", + { + input: [ + { + type: "reasoning", + summary: [{ type: "summary_text", text: "Display-only summary" }], + }, + { type: "function_call", call_id: "call_1", name: "search", arguments: "{}" }, + ], + }, + false, + { _preserveReasoningContent: true } + ) as { messages: Array> }; + + assert.equal(result.messages[0].reasoning_content, undefined); +}); + +test("Responses -> Chat rejects opaque reasoning instead of replaying its plaintext companion", () => { + assert.throws( + () => + openaiResponsesToOpenAIRequest( + "deepseek-v4-pro", + { + input: [ + { + id: "rs_opaque", + type: "reasoning", + encrypted_content: "opaque-provider-state", + content: [{ type: "reasoning_text", text: "Untrusted plaintext companion" }], + summary: [{ type: "summary_text", text: "Display summary" }], + }, + { type: "function_call", call_id: "call_1", name: "search", arguments: "{}" }, + ], + }, + false, + { _preserveReasoningContent: true } + ), + /Reasoning continuation is not compatible/ + ); +}); + test("Responses -> Chat merges assistant text that follows a function call", () => { const result = openaiResponsesToOpenAIRequest( "gpt-4o", @@ -146,7 +209,7 @@ test("Responses -> Chat merges assistant text that follows a function call", () }, { type: "reasoning", - summary: [{ type: "summary_text", text: "Inspection complete" }], + content: [{ type: "reasoning_text", text: "Inspection complete" }], }, { type: "message", role: "user", content: [{ type: "input_text", text: "Continue" }] }, ], @@ -384,6 +447,103 @@ test("Chat -> Responses clamps call_id to 64 chars and keeps the pair matched (p ); }); +test("Chat -> Responses defaults unannotated targets to plaintext reasoning", () => { + const result = openaiToOpenAIResponsesRequest( + "deepseek-v4-flash", + { + messages: [ + { + role: "assistant", + content: null, + reasoning_content: "Inspect the repository first", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "search", arguments: "{}" }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", content: "found" }, + ], + }, + false, + { _provider: "opencode-go" } + ) as { input: Array> }; + + assert.deepEqual(result.input, [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "Inspect the repository first" }], + }, + { + type: "function_call", + call_id: "call_1", + name: "search", + arguments: "{}", + status: "completed", + }, + { type: "function_call_output", call_id: "call_1", output: "found", status: "completed" }, + ]); +}); + +test("Chat -> DeepSeek Responses accepts the plaintext reasoning alias", () => { + const result = openaiToOpenAIResponsesRequest( + "deepseek-v4-pro", + { + messages: [ + { + role: "assistant", + content: null, + reasoning: "Alias plaintext reasoning", + tool_calls: [ + { + id: "call_alias", + type: "function", + function: { name: "search", arguments: "{}" }, + }, + ], + }, + ], + }, + false, + { _provider: "deepseek" } + ) as { input: Array> }; + + assert.deepEqual(result.input[0], { + type: "reasoning", + content: [{ type: "reasoning_text", text: "Alias plaintext reasoning" }], + }); +}); + +test("Chat -> Responses never promotes OmniRoute's internal reasoning placeholder", () => { + const result = openaiToOpenAIResponsesRequest( + "deepseek-v4-pro", + { + messages: [ + { + role: "assistant", + reasoning_content: "(prior reasoning summary unavailable)", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "search", arguments: "{}" }, + }, + ], + }, + ], + }, + false, + { _provider: "deepseek" } + ) as { input: Array> }; + + assert.equal( + result.input.some((item) => item.type === "reasoning"), + false + ); +}); + test("Chat -> Responses converts messages, tool calls, tool outputs, tools and pass-through params", () => { const result = openaiToOpenAIResponsesRequest( "gpt-4o", diff --git a/tests/unit/ui/edit-connection-modal-free-models.test.tsx b/tests/unit/ui/edit-connection-modal-free-models.test.tsx index 0e7abd35c3..9445443a53 100644 --- a/tests/unit/ui/edit-connection-modal-free-models.test.tsx +++ b/tests/unit/ui/edit-connection-modal-free-models.test.tsx @@ -195,7 +195,7 @@ describe("EditConnectionModal — encrypted Responses reasoning", () => { expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(false); }); - it("defaults off and persists an opt-in for first-party OpenAI", async () => { + it("hides the redundant opt-in for first-party OpenAI and removes stale state on save", async () => { const onSave = vi.fn().mockResolvedValue(undefined); const el = render({ providerId: "openai", @@ -203,19 +203,19 @@ describe("EditConnectionModal — encrypted Responses reasoning", () => { id: "conn-openai", provider: "openai", authType: "apikey", - providerSpecificData: {}, + providerSpecificData: { preserveEncryptedReasoning: true }, }, onSave, }); - const toggle = el.querySelector(PRESERVE_TOGGLE)!; - expect(toggle.getAttribute("aria-checked")).toBe("false"); - act(() => toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(el.querySelector(PRESERVE_TOGGLE)).toBeNull(); const saveBtn = Array.from(el.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "save" )!; act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }))); await waitFor(() => onSave.mock.calls.length > 0); - expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(true); + expect(onSave.mock.calls[0][0].providerSpecificData).not.toHaveProperty( + "preserveEncryptedReasoning" + ); }); it("is absent for a chat-only compatible connection", () => { @@ -244,7 +244,7 @@ describe("EditConnectionModal — encrypted Responses reasoning", () => { expect(el.querySelector(PRESERVE_TOGGLE)?.getAttribute("aria-checked")).toBe("false"); }); - it("keeps Codex controls and persists the opt-in on its OAuth save path", async () => { + it("hides the redundant opt-in for Codex while keeping its effective controls", async () => { const onSave = vi.fn().mockResolvedValue(undefined); const el = render({ providerId: "codex", @@ -256,22 +256,19 @@ describe("EditConnectionModal — encrypted Responses reasoning", () => { }, onSave, }); - expect(el.querySelector(PRESERVE_TOGGLE)?.getAttribute("aria-checked")).toBe("true"); + expect(el.querySelector(PRESERVE_TOGGLE)).toBeNull(); expect(el.textContent).toContain("defaultThinkingStrengthLabel"); expect( el.querySelector('button[role="switch"][aria-label="openaiResponsesStoreLabel"]') ).toBeTruthy(); - const cooldownToggle = el.querySelector( - 'button[role="switch"][aria-label="disableCoolingLabel"]' - )!; - const reasoningToggle = el.querySelector(PRESERVE_TOGGLE)!; - expect(reasoningToggle.parentElement?.nextElementSibling).toBe(cooldownToggle.parentElement); const saveBtn = Array.from(el.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "save" )!; act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }))); await waitFor(() => onSave.mock.calls.length > 0); - expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(true); + expect(onSave.mock.calls[0][0].providerSpecificData).not.toHaveProperty( + "preserveEncryptedReasoning" + ); }); }); From 00aba8ef16e946451a10e2f01293f23e34dce5c0 Mon Sep 17 00:00:00 2001 From: Nahuel Saruf <86387896+NahuSaruf@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:33:39 -0300 Subject: [PATCH 133/135] fix(sse): include prompt cache usage fields on message_stop fallback path (#10545) Mirrors the existing prompt_tokens_details cache-field mapping from the message_delta finish path into the message_stop fallback, so OpenAI-compatible clients see cache_read/cache_creation counters when the finish signal arrives without usage on the same event. Fixes #10535. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 2 files): - 16/16 focused tests pass (translator-resp-claude-to-openai.test.ts), including the new regression test for this exact fallback path. - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: NahuSaruf --- .../translator/response/claude-to-openai.ts | 12 +++++++ .../translator-resp-claude-to-openai.test.ts | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/open-sse/translator/response/claude-to-openai.ts b/open-sse/translator/response/claude-to-openai.ts index 4905bd8e60..2d20661e7b 100644 --- a/open-sse/translator/response/claude-to-openai.ts +++ b/open-sse/translator/response/claude-to-openai.ts @@ -332,6 +332,8 @@ export function claudeToOpenAIResponse(chunk, state) { if (!state.finishReasonSent) { const finishReason = state.finishReason || (state.toolCalls?.size > 0 ? "tool_calls" : "stop"); + const cachedTokens = state.usage?.cache_read_input_tokens || 0; + const cacheCreationTokens = state.usage?.cache_creation_input_tokens || 0; const usageObj = state.usage && typeof state.usage === "object" ? { @@ -347,6 +349,16 @@ export function claudeToOpenAIResponse(chunk, state) { }, } : {}), + ...(cachedTokens > 0 || cacheCreationTokens > 0 + ? { + prompt_tokens_details: { + ...(cachedTokens > 0 ? { cached_tokens: cachedTokens } : {}), + ...(cacheCreationTokens > 0 + ? { cache_creation_tokens: cacheCreationTokens } + : {}), + }, + } + : {}), }, } : {}; diff --git a/tests/unit/translator-resp-claude-to-openai.test.ts b/tests/unit/translator-resp-claude-to-openai.test.ts index 591622febf..67a242d39f 100644 --- a/tests/unit/translator-resp-claude-to-openai.test.ts +++ b/tests/unit/translator-resp-claude-to-openai.test.ts @@ -403,6 +403,40 @@ test("Claude stream: message_stop falls back to tool_calls when tool use already assert.equal(result[0].choices[0].finish_reason, "tool_calls"); }); +test("Claude stream: message_stop includes prompt_tokens_details when usage arrived on an earlier message_delta without stop_reason (#10535)", () => { + const state = createState(); + claudeToOpenAIResponse( + { type: "message_start", message: { id: "msg1", model: "claude-sonnet-4-6" } }, + state + ); + + // Usage lands on a message_delta that carries no stop_reason (e.g. an + // upstream that reports usage and the finish signal in separate events), + // so the finalChunk branch in the message_delta case never runs and + // finishReasonSent stays false. + const deltaResult = claudeToOpenAIResponse( + { + type: "message_delta", + delta: {}, + usage: { + input_tokens: 8, + output_tokens: 5, + cache_read_input_tokens: 2000, + cache_creation_input_tokens: 0, + }, + }, + state + ); + assert.equal(deltaResult, null); + + const result = claudeToOpenAIResponse({ type: "message_stop" }, state); + + assert.equal(result[0].usage.prompt_tokens, 2008); + assert.equal(result[0].usage.completion_tokens, 5); + assert.equal(result[0].usage.total_tokens, 2013); + assert.equal(result[0].usage.prompt_tokens_details.cached_tokens, 2000); +}); + test("Claude stream: unsupported events return null", () => { assert.equal(claudeToOpenAIResponse({ type: "error" }, createState()), null); }); From 28788cb9aff6315b62086a96ce56d552dd8923c2 Mon Sep 17 00:00:00 2001 From: Nathan <370788475@qq.com> Date: Fri, 21 Aug 2026 09:41:27 +0800 Subject: [PATCH 134/135] fix(cli): route provider tests through connection API (#10572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns provider-test CLI paths with the server's connection-owned management API: `omniroute test` now resolves a connection and calls `POST /api/providers/{id}/test` instead of the missing `/api/v1/providers/test` route; `--all-providers` carries exact connection ids into both non-interactive and TUI runs. Fixes #10570. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 4 files): - 42/42 focused tests pass (cli-provider-test-routes-10570, cli-providers-command, cli-providers-rotate, cli-route-unavailable-fallback-10081, cli-expanded-commands). - One pre-existing test in cli-expanded-commands.test.ts (not touched by the PR) mocked the old route and the old `success` response field, exposed only after merging with the current release tip — fixed the mock to match the new per-connection route and the `valid` field the real route actually returns, pushed fix-in-place to the PR branch (owner-authorized rule: fix-in-place over reimplementation, credit preserved). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: hydraxman --- CHANGELOG.md | 1 + bin/cli/commands/providers.mjs | 35 +++- bin/cli/commands/test-provider.mjs | 87 ++++++--- bin/cli/tui/ProvidersTestAll.jsx | 24 +-- tests/unit/cli-expanded-commands.test.ts | 4 +- .../cli-provider-test-routes-10570.test.ts | 177 ++++++++++++++++++ 6 files changed, 290 insertions(+), 38 deletions(-) create mode 100644 tests/unit/cli-provider-test-routes-10570.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 94438a0922..9c01b39a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,6 +169,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e - **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963 - **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366) +- **cli**: route provider test commands through configured connection test endpoints (#10570) - **executors**: fix internal timeout misclassified as client disconnect (499) for 7 niche executors — pass TimeoutError reason to controller.abort() (#8197 side-finding) - test(combo): guard auto/best-free never leaks the combo name as a model (#7754) - fix(vision-bridge): describe-model no longer returns unreachable "openai/gpt-4o-mini" when every vision-capable provider is unreachable on the instance — returns null instead and surfaces a clear error (#8430) diff --git a/bin/cli/commands/providers.mjs b/bin/cli/commands/providers.mjs index 91d60cead8..eb872241bf 100644 --- a/bin/cli/commands/providers.mjs +++ b/bin/cli/commands/providers.mjs @@ -129,7 +129,34 @@ function buildTestInput(connection, apiKey) { }; } -async function runProviderTest(db, connection) { +async function testProviderConnectionThroughServer(connection) { + try { + const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}/test`, { + method: "POST", + body: {}, + retry: false, + timeout: 30000, + acceptNotOk: true, + }); + const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` }; + return { + connection: publicConnection(connection), + ...data, + valid: data.valid === true, + skipped: false, + }; + } catch (error) { + return { + connection: publicConnection(connection), + valid: false, + skipped: false, + error: error instanceof Error ? error.message : String(error), + statusCode: null, + }; + } +} + +async function runProviderTest(db, connection, { serverUp = false } = {}) { // Only API-key connections can be probed with a stored credential. OAuth / // no-auth connections have nothing for testProviderApiKey() to send, and // getProviderApiKey() throws for them by design — reporting that as a FAILED @@ -151,6 +178,9 @@ async function runProviderTest(db, connection) { // means the CLI has no probe recipe, not that the provider is unhealthy. // Persisting it would overwrite a good test_status with a failure. if (result.unsupported) { + if (serverUp) { + return testProviderConnectionThroughServer(connection); + } return { connection: publicConnection(connection), ...result, @@ -266,6 +296,7 @@ export async function runTestCommand(selector, opts = {}) { } export async function runTestAllCommand(opts = {}) { + const serverUp = await isServerUp(); const { db } = await openOmniRouteDb(); try { const connections = listProviderConnections(db); @@ -280,7 +311,7 @@ export async function runTestAllCommand(opts = {}) { }); continue; } - results.push(await runProviderTest(db, connection)); + results.push(await runProviderTest(db, connection, { serverUp })); } if (opts.json) { diff --git a/bin/cli/commands/test-provider.mjs b/bin/cli/commands/test-provider.mjs index 8802f75cd1..ec10c24649 100644 --- a/bin/cli/commands/test-provider.mjs +++ b/bin/cli/commands/test-provider.mjs @@ -38,12 +38,19 @@ export async function runTestProviderCommand(provider, model, opts = {}) { } const targetProvider = provider || "anthropic"; - const targetModel = model || "claude-haiku-4-5-20251001"; + const connections = await _loadConnections(); + if (!connections) return 1; + const connection = _resolveConnection(connections, targetProvider, model); + if (!connection) { + console.error(`Provider connection not found: ${targetProvider}`); + return 1; + } + const targetModel = model || connection.defaultModel; const repeat = opts.repeat && opts.repeat > 0 ? opts.repeat : 1; const results = []; for (let i = 0; i < repeat; i++) { - const result = await _runSingleTest(targetProvider, targetModel); + const result = await _runSingleTest(connection, targetModel); results.push(result); } @@ -70,18 +77,10 @@ export async function runTestProviderCommand(provider, model, opts = {}) { } async function _runAllProviders(opts) { - const res = await apiFetch("/api/providers?limit=200", { - retry: false, - timeout: 5000, - acceptNotOk: true, - }); - if (!res.ok) { - console.error(t("test.noServer")); - return 1; - } - const data = await res.json(); - const connections = (data.connections ?? data.providers ?? data.items ?? data).filter( - (c) => c.authType === "apikey" || c.testStatus !== "unavailable" + const loaded = await _loadConnections(); + if (!loaded) return 1; + const connections = loaded.filter( + (c) => c.isActive !== false && (c.authType === "apikey" || c.testStatus !== "unavailable") ); if (connections.length === 0) { console.log(t("test.noProviders")); @@ -89,6 +88,7 @@ async function _runAllProviders(opts) { } const providers = connections.map((c) => ({ + connectionId: c.id, provider: c.provider ?? c.id, model: c.defaultModel ?? c.model, })); @@ -102,8 +102,8 @@ async function _runAllProviders(opts) { } const results = await Promise.all( - providers.map(async ({ provider, model }) => { - const r = await _runSingleTest(provider, model); + providers.map(async ({ connectionId, provider, model }) => { + const r = await _runSingleTest({ id: connectionId }, model); return { provider, model, ...r }; }) ); @@ -123,6 +123,13 @@ async function _runAllProviders(opts) { async function _runCompare(provider, opts) { const targetProvider = provider || "anthropic"; + const connections = await _loadConnections(); + if (!connections) return 1; + const connection = _resolveConnection(connections, targetProvider); + if (!connection) { + console.error(`Provider connection not found: ${targetProvider}`); + return 1; + } const models = opts.compare .split(",") .map((m) => m.trim()) @@ -138,7 +145,7 @@ async function _runCompare(provider, opts) { for (const model of models) { const results = []; for (let i = 0; i < repeat; i++) { - const result = await _runSingleTest(targetProvider, model); + const result = await _runSingleTest(connection, model); results.push(result); } rows.push({ model, ..._aggregate(results, true) }); @@ -180,19 +187,55 @@ async function _runCompare(provider, opts) { return rows.every((r) => r.success) ? 0 : 1; } -async function _runSingleTest(provider, model) { +async function _loadConnections() { + const res = await apiFetch("/api/providers?limit=200", { + retry: false, + timeout: 5000, + acceptNotOk: true, + }); + if (!res.ok) { + console.error(t("test.noServer")); + return null; + } + const data = await res.json(); + const connections = data.connections ?? data.providers ?? data.items ?? data; + if (!Array.isArray(connections)) { + console.error(t("test.noServer")); + return null; + } + return connections; +} + +function _resolveConnection(connections, selector, model) { + const normalized = String(selector || "") + .trim() + .toLowerCase(); + const active = connections.filter((connection) => connection.isActive !== false); + return ( + active.find((connection) => String(connection.id || "").toLowerCase() === normalized) ?? + active.find((connection) => String(connection.name || "").toLowerCase() === normalized) ?? + active.find( + (connection) => + String(connection.provider || "").toLowerCase() === normalized && + (!model || connection.defaultModel === model || connection.model === model) + ) ?? + active.find((connection) => String(connection.provider || "").toLowerCase() === normalized) + ); +} + +async function _runSingleTest(connection, model) { const startMs = Date.now(); try { - const res = await apiFetch("/api/v1/providers/test", { + const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}/test`, { method: "POST", - body: { provider, model }, + body: model ? { validationModelId: model } : {}, retry: false, timeout: 30000, acceptNotOk: true, }); const durationMs = Date.now() - startMs; - const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` }; - return { ...data, durationMs }; + const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` }; + return { ...data, success: data.valid === true, durationMs }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); return { diff --git a/bin/cli/tui/ProvidersTestAll.jsx b/bin/cli/tui/ProvidersTestAll.jsx index 73fc78614a..c1911888ac 100644 --- a/bin/cli/tui/ProvidersTestAll.jsx +++ b/bin/cli/tui/ProvidersTestAll.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { render, Box, Text, useInput } from "ink"; import Spinner from "ink-spinner"; +import { apiFetch } from "../api.mjs"; import { DataTable } from "../tui-components/DataTable.jsx"; import { ProgressBar } from "../tui-components/ProgressBar.jsx"; @@ -31,22 +32,20 @@ const TABLE_SCHEMA = [ { key: "error", header: "Error", width: 28, formatter: (v) => (v ? v.slice(0, 26) : "") }, ]; -async function testOne(provider, model, baseUrl, apiKey) { - const headers = { - "Content-Type": "application/json", - ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), - }; +async function testOne(connectionId, model, baseUrl, apiKey) { const start = Date.now(); try { - const res = await fetch(`${baseUrl}/api/v1/providers/test`, { + const res = await apiFetch(`/api/providers/${encodeURIComponent(connectionId)}/test`, { method: "POST", - headers, - body: JSON.stringify({ provider, model }), - signal: AbortSignal.timeout(30000), + body: model ? { validationModelId: model } : {}, + baseUrl, + token: apiKey, + timeout: 30000, + acceptNotOk: true, }); const latencyMs = Date.now() - start; - const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` }; - return { status: data.success ? STATUS.PASS : STATUS.FAIL, latencyMs, error: data.error }; + const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` }; + return { status: data.valid ? STATUS.PASS : STATUS.FAIL, latencyMs, error: data.error }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); return { @@ -63,6 +62,7 @@ function ProvidersTestAllApp({ providers, baseUrl, apiKey, concurrency = 4, onEx const [rows, setRows] = useState(() => providers.map((p, i) => ({ id: i, + connectionId: p.connectionId ?? p.id, provider: p.provider ?? p.id ?? String(p), model: p.model ?? p.defaultModel ?? "", status: STATUS.PENDING, @@ -91,7 +91,7 @@ function ProvidersTestAllApp({ providers, baseUrl, apiKey, concurrency = 4, onEx const row = queue[cursor++]; running++; update(row.id, { status: STATUS.RUNNING }); - testOne(row.provider, row.model, resolved, apiKey).then((result) => { + testOne(row.connectionId, row.model, resolved, apiKey).then((result) => { update(row.id, result); running--; nextSlot(); diff --git a/tests/unit/cli-expanded-commands.test.ts b/tests/unit/cli-expanded-commands.test.ts index 1327351555..e631a084c6 100644 --- a/tests/unit/cli-expanded-commands.test.ts +++ b/tests/unit/cli-expanded-commands.test.ts @@ -319,8 +319,8 @@ test("test-provider --all-providers consumes the connections envelope", async () if (url.includes("/api/providers?limit=200")) { return Promise.resolve(new Response(JSON.stringify({ connections }), { status: 200 })); } - if (url.includes("/api/v1/providers/test")) { - return Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 201 })); + if (url.includes("/api/providers/") && url.includes("/test")) { + return Promise.resolve(new Response(JSON.stringify({ valid: true }), { status: 200 })); } throw new Error(`unexpected URL: ${url}`); }) as typeof fetch; diff --git a/tests/unit/cli-provider-test-routes-10570.test.ts b/tests/unit/cli-provider-test-routes-10570.test.ts new file mode 100644 index 0000000000..386a3e448f --- /dev/null +++ b/tests/unit/cli-provider-test-routes-10570.test.ts @@ -0,0 +1,177 @@ +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"; +import Database from "better-sqlite3"; + +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +const ORIGINAL_FETCH = globalThis.fetch; +const ORIGINAL_API_KEY = process.env.OMNIROUTE_API_KEY; + +function jsonResponse(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +async function withCliEnv(fn: (dataDir: string) => Promise) { + const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cli-routes-10570-")); + process.env.DATA_DIR = dataDir; + process.env.OMNIROUTE_API_KEY = "test-management-key"; + delete process.env.STORAGE_ENCRYPTION_KEY; + try { + await fn(dataDir); + } finally { + globalThis.fetch = ORIGINAL_FETCH; + fs.rmSync(dataDir, { recursive: true, force: true }); + if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = ORIGINAL_DATA_DIR; + if (ORIGINAL_API_KEY === undefined) delete process.env.OMNIROUTE_API_KEY; + else process.env.OMNIROUTE_API_KEY = ORIGINAL_API_KEY; + } +} + +async function createConnection( + dataDir: string, + input: { provider?: string; name?: string; apiKey?: string } = {} +) { + const { ensureProviderSchema, upsertApiKeyProviderConnection } = + await import("../../bin/cli/provider-store.mjs"); + const db = new Database(path.join(dataDir, "storage.sqlite")); + ensureProviderSchema(db); + const connection = upsertApiKeyProviderConnection(db, { + provider: input.provider ?? "custom-openai-compatible", + name: input.name ?? "Custom Connection", + apiKey: input.apiKey ?? "test-key", + }); + db.close(); + return connection; +} + +test("omniroute test resolves a connection and calls its server-owned test route", async () => { + await withCliEnv(async () => { + const requests: Array<{ path: string; method: string }> = []; + globalThis.fetch = (async (input, init) => { + const url = new URL(String(input)); + const method = String(init?.method ?? "GET").toUpperCase(); + requests.push({ path: `${url.pathname}${url.search}`, method }); + if (url.pathname === "/api/health") return jsonResponse({ status: "ok" }); + if (url.pathname === "/api/providers") { + return jsonResponse({ + connections: [ + { + id: "conn/custom 1", + provider: "custom-openai-compatible", + name: "Custom Connection", + authType: "apikey", + isActive: true, + defaultModel: "custom-model", + }, + ], + total: 1, + }); + } + if (url.pathname === "/api/providers/conn%2Fcustom%201/test" && method === "POST") { + return jsonResponse({ valid: true, error: null, latencyMs: 7 }); + } + return jsonResponse({ error: "unexpected route" }, 404); + }) as typeof fetch; + + const { runTestProviderCommand } = await import("../../bin/cli/commands/test-provider.mjs"); + const exitCode = await runTestProviderCommand("custom-openai-compatible", undefined, { + json: true, + }); + + assert.equal(exitCode, 0); + assert.deepEqual(requests, [ + { path: "/api/health", method: "GET" }, + { path: "/api/providers?limit=200", method: "GET" }, + { path: "/api/providers/conn%2Fcustom%201/test", method: "POST" }, + ]); + }); +}); + +test("omniroute test --all-providers consumes the current connections response shape", async () => { + await withCliEnv(async () => { + const requests: Array<{ path: string; method: string }> = []; + globalThis.fetch = (async (input, init) => { + const url = new URL(String(input)); + const method = String(init?.method ?? "GET").toUpperCase(); + requests.push({ path: `${url.pathname}${url.search}`, method }); + if (url.pathname === "/api/health") return jsonResponse({ status: "ok" }); + if (url.pathname === "/api/providers") { + return jsonResponse({ + connections: [ + { + id: "conn-all-1", + provider: "custom-openai-compatible", + name: "Custom Connection", + authType: "apikey", + isActive: true, + defaultModel: "custom-model", + }, + ], + total: 1, + }); + } + if (url.pathname === "/api/providers/conn-all-1/test" && method === "POST") { + return jsonResponse({ valid: true, error: null }); + } + return jsonResponse({ error: "unexpected route" }, 404); + }) as typeof fetch; + + const { runTestProviderCommand } = await import("../../bin/cli/commands/test-provider.mjs"); + const exitCode = await runTestProviderCommand(undefined, undefined, { + allProviders: true, + json: true, + }); + + assert.equal(exitCode, 0); + assert.deepEqual(requests, [ + { path: "/api/health", method: "GET" }, + { path: "/api/providers?limit=200", method: "GET" }, + { path: "/api/providers/conn-all-1/test", method: "POST" }, + ]); + }); +}); + +test("the interactive all-provider view uses the same connection-owned test route", async () => { + const source = await fs.promises.readFile( + new URL("../../bin/cli/tui/ProvidersTestAll.jsx", import.meta.url), + "utf8" + ); + assert.match(source, /import \{ apiFetch \} from "\.\.\/api\.mjs"/); + assert.match(source, /connectionId: p\.connectionId \?\? p\.id/); + assert.match(source, /apiFetch\(/); + assert.match(source, /\/api\/providers\/\$\{encodeURIComponent\(connectionId\)\}\/test/); + assert.match(source, /data\.valid/); + assert.doesNotMatch(source, /api\/v1\/providers\/test/); +}); + +test("providers test-all falls back to the server for unsupported custom API-key providers", async () => { + await withCliEnv(async (dataDir) => { + const connection = await createConnection(dataDir); + const requests: Array<{ path: string; method: string }> = []; + globalThis.fetch = (async (input, init) => { + const url = new URL(String(input)); + const method = String(init?.method ?? "GET").toUpperCase(); + requests.push({ path: url.pathname, method }); + if (url.pathname === "/api/health") return jsonResponse({ status: "ok" }); + if (url.pathname === `/api/providers/${connection.id}/test` && method === "POST") { + return jsonResponse({ valid: true, error: null, latencyMs: 9 }); + } + return jsonResponse({ error: "unexpected route" }, 404); + }) as typeof fetch; + + const { runTestAllCommand } = await import("../../bin/cli/commands/providers.mjs"); + const exitCode = await runTestAllCommand({ json: true }); + + assert.equal(exitCode, 0); + assert.deepEqual(requests, [ + { path: "/api/health", method: "GET" }, + { path: `/api/providers/${connection.id}/test`, method: "POST" }, + ]); + }); +}); From cbf23772ec2d9842420ff454f599b1a5a2884602 Mon Sep 17 00:00:00 2001 From: "Adrian A. Firmansyah" <34051784+adrianaryaputra@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:53:24 +0700 Subject: [PATCH 135/135] feat(providers): implement native support for Freebuff AI gateway (#6793) (#10531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds native support for Freebuff (Codebuff CLI free-tier gateway): executor with upstream session acquisition, agent-run lifecycle (START/FINISH), canonical system-prompt injection, and model→agent mapping for 9 free models; registry entry, dashboard branding/icons, and API-key validation. Closes #6793. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts after resolving generated provider-count drift, 15 files): - 9/9 focused tests pass (freebuff-provider, providers-constants-split). - Dropped one out-of-scope, unrelated hunk in scripts/build/build-next-isolated.mjs (build-memory heap tuning) that had nothing to do with the Freebuff provider — kept the branch scoped to its stated purpose. - check-changelog-integrity: OK. - file-size: gateways.ts and AddApiKeyModal.tsx crossed the frozen cap by +15/+5 lines (irreducible catalog-entry + credential-hint additions) — rebaselined with justification, pushed fix-in-place to the PR branch. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: adrianaryaputra --- config/quality/file-size-baseline.json | 5 +- open-sse/config/providers/index.ts | 2 + .../providers/registry/freebuff/index.ts | 70 +++++++ open-sse/executors/freebuff.ts | 172 ++++++++++++++++++ open-sse/executors/index.ts | 3 + public/providers/freebuff-dark.svg | 7 + public/providers/freebuff-light.svg | 7 + public/providers/freebuff.png | Bin 0 -> 5969 bytes public/providers/freebuff.svg | 7 + .../[id]/components/modals/AddApiKeyModal.tsx | 15 +- src/lib/providers/validation.ts | 32 ++++ src/shared/components/ProviderIcon.tsx | 1 + .../constants/providers/apikey/gateways.ts | 15 ++ tests/snapshots/provider/translate-path.json | 23 +++ tests/unit/freebuff-provider.test.ts | 59 ++++++ tests/unit/providers-constants-split.test.ts | 13 +- 16 files changed, 418 insertions(+), 13 deletions(-) create mode 100644 open-sse/config/providers/registry/freebuff/index.ts create mode 100644 open-sse/executors/freebuff.ts create mode 100644 public/providers/freebuff-dark.svg create mode 100644 public/providers/freebuff-light.svg create mode 100644 public/providers/freebuff.png create mode 100644 public/providers/freebuff.svg create mode 100644 tests/unit/freebuff-provider.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index f19fc0ea0b..72bc030875 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).", "_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.", "_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).", "_rebaseline_2026_08_13_10243_codex_fingerprint_merge": "PR #10243 (xz-dev, Codex OAuth fingerprint convergence) merge into release/v3.8.50: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts crossed the 1000-line new-file cap for the first time (974 on base, 997 on the PR's own branch, 1013 after merging + prettier reflow) purely from combining two independent, already-legitimate feature additions that landed on the same shared UI-helper file — this PR's own Codex fingerprint-mode select/toggle wiring (CODEX_FINGERPRINT_MODE_VALUES, getCodexFingerprintModeLabel, CodexFingerprintModeValue) plus #8949's unrelated Codex account-service-tier helpers merged concurrently on release/v3.8.50. Neither addition alone crosses the cap; git's line-level auto-merge does not detect a threshold crossing. Not modularized as part of this conflict-resolution merge commit (out of scope — this is a merge, not a feature change). Covered by the PR's own tests/unit/codex-fingerprint-convergence.test.ts, tests/unit/executor-codex.test.ts, tests/unit/provider-specific-data-schema.test.ts (all passing post-merge).", @@ -442,10 +443,10 @@ "src/shared/components/ModelSelectModal.tsx": 1138, "src/shared/constants/providers/apikey/gateways.ts": 1250 }, - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1062, + "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1067, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051, "src/shared/components/ModelSelectModal.tsx": 1138, - "src/shared/constants/providers/apikey/gateways.ts": 1283, + "src/shared/constants/providers/apikey/gateways.ts": 1298, "open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387, "_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).", "src/lib/modelCapabilities.ts": 1006, diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index a7067a022e..00861b2422 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -18,6 +18,7 @@ import { deepaiProvider } from "./registry/deepai/index.ts"; import { upstageProvider } from "./registry/upstage/index.ts"; import { nebiusProvider } from "./registry/nebius/index.ts"; import { fireworksProvider } from "./registry/fireworks/index.ts"; +import { freebuffProvider } from "./registry/freebuff/index.ts"; import { llamagateProvider } from "./registry/llamagate/index.ts"; import { glmProvider } from "./registry/glm/index.ts"; import { glmtProvider } from "./registry/glm/t/index.ts"; @@ -282,6 +283,7 @@ export const REGISTRY: Record = { deepai: deepaiProvider, nebius: nebiusProvider, fireworks: fireworksProvider, + freebuff: freebuffProvider, llamagate: llamagateProvider, glm: glmProvider, glmt: glmtProvider, diff --git a/open-sse/config/providers/registry/freebuff/index.ts b/open-sse/config/providers/registry/freebuff/index.ts new file mode 100644 index 0000000000..713f469548 --- /dev/null +++ b/open-sse/config/providers/registry/freebuff/index.ts @@ -0,0 +1,70 @@ +import type { RegistryEntry } from "../../shared.ts"; + +export const freebuffProvider: RegistryEntry = { + id: "freebuff", + alias: "fb", + format: "openai", + executor: "freebuff", + baseUrl: "https://www.codebuff.com/api/v1", + authType: "apikey", + authHeader: "bearer", + models: [ + { + id: "deepseek/deepseek-v4-flash", + name: "DeepSeek V4 Flash", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "deepseek/deepseek-v4-pro", + name: "DeepSeek V4 Pro", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "openai/gpt-5.6-luna", + name: "GPT-5.6 Luna", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "minimax/minimax-m3", + name: "MiniMax M3", + supportsVision: true, + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "mimo/mimo-v2.5", + name: "MiMo v2.5", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "z-ai/glm-5.2", + name: "GLM 5.2", + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "crof/kimi-k3-eco", + name: "Kimi K3 Eco", + supportsVision: true, + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "anthropic/claude-fable-5", + name: "Claude Fable 5", + supportsVision: true, + supportsReasoning: true, + contextLength: 131_072, + }, + { + id: "meta/muse-spark-1.2-contributor", + name: "Meta Muse Spark 1.2 Contributor", + supportsReasoning: true, + contextLength: 131_072, + }, + ], +}; diff --git a/open-sse/executors/freebuff.ts b/open-sse/executors/freebuff.ts new file mode 100644 index 0000000000..bc2de30f63 --- /dev/null +++ b/open-sse/executors/freebuff.ts @@ -0,0 +1,172 @@ +import { + BaseExecutor, + type ExecuteInput, +} from "./base.ts"; +import { PROVIDERS } from "../config/constants.ts"; + +const MODEL_TO_AGENT: Record = { + "deepseek/deepseek-v4-flash": "base2-free-deepseek-flash", + "deepseek/deepseek-v4-pro": "base2-free-deepseek", + "openai/gpt-5.6-luna": "base2-free-luna", + "minimax/minimax-m3": "base2-free-minimax-m3", + "mimo/mimo-v2.5": "base2-free-mimo", + "z-ai/glm-5.2": "base2-free-glm", + "crof/kimi-k3-eco": "base2-free-kimi-k3-eco", + "anthropic/claude-fable-5": "base2-free-fable", + "meta/muse-spark-1.2-contributor": "base2-free-muse-spark", +}; + +function generateClientSessionId(): string { + const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; + let out = ""; + for (let i = 0; i < 13; i++) { + out += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return out; +} + +export class FreebuffExecutor extends BaseExecutor { + constructor() { + super("freebuff", (PROVIDERS as Record).freebuff as string || "freebuff"); + } + + override async execute(input: ExecuteInput) { + const { model, body, stream, credentials, signal } = input; + const token = credentials?.apiKey || credentials?.accessToken || ""; + + if (!token) { + return { + response: new Response( + JSON.stringify({ error: { message: "Freebuff Auth Token required", type: "authentication_error" } }), + { status: 401, headers: { "Content-Type": "application/json" } } + ), + }; + } + + const requestedModel = typeof model === "string" ? model.replace(/^freebuff\//, "") : (model || "deepseek/deepseek-v4-flash"); + const agentId = MODEL_TO_AGENT[requestedModel] || "base2-free"; + + const authHeaders = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "User-Agent": "codebuff/0.1.0 (darwin-arm64)", + }; + + let instanceId = ""; + let runId = ""; + + // 1. Session acquisition + try { + const sessionRes = await fetch("https://www.codebuff.com/api/v1/freebuff/session", { + method: "POST", + headers: { + ...authHeaders, + "x-freebuff-model": requestedModel, + }, + body: JSON.stringify({}), + signal, + }); + if (sessionRes.ok) { + const data = (await sessionRes.json()) as { instanceId?: string }; + instanceId = data.instanceId || ""; + } else { + const errText = await sessionRes.text(); + return { + response: new Response( + JSON.stringify({ error: { message: `Freebuff session failed (${sessionRes.status}): ${errText}`, type: "upstream_error" } }), + { status: sessionRes.status, headers: { "Content-Type": "application/json" } } + ), + }; + } + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + return { + response: new Response( + JSON.stringify({ error: { message: `Freebuff session network error: ${msg}`, type: "upstream_error" } }), + { status: 502, headers: { "Content-Type": "application/json" } } + ), + }; + } + + // 2. Start agent run + try { + const runRes = await fetch("https://www.codebuff.com/api/v1/agent-runs", { + method: "POST", + headers: authHeaders, + body: JSON.stringify({ action: "START", agentId }), + signal, + }); + if (runRes.ok) { + const runData = (await runRes.json()) as { runId?: string }; + runId = runData.runId || ""; + } + } catch {} + + // 3. Prepare Chat Payload & Buffy System Prompt + const incomingMessages = Array.isArray(body?.messages) ? [...body.messages] : []; + const hasBuffyPrompt = + incomingMessages.length > 0 && + incomingMessages[0].role === "system" && + typeof incomingMessages[0].content === "string" && + incomingMessages[0].content.trim().startsWith("You are Buffy"); + + if (!hasBuffyPrompt) { + incomingMessages.unshift({ + role: "system", + content: "You are Buffy, the strategic coding assistant.", + }); + } + + const clientSessionId = generateClientSessionId(); + const upstreamBody = { + ...(body || {}), + model: requestedModel, + messages: incomingMessages, + stream: stream !== false, + codebuff_metadata: { + run_id: runId, + cost_mode: "free", + client_id: clientSessionId, + freebuff_instance_id: instanceId, + ...((body as Record)?.codebuff_metadata as Record || {}), + }, + }; + + const completionHeaders = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "User-Agent": "ai-sdk/openai-compatible/1.0.25/codebuff", + Accept: "application/json, text/event-stream", + "x-freebuff-instance-id": instanceId, + ...(runId ? { "x-codebuff-run-id": runId } : {}), + "x-codebuff-agent-id": agentId, + }; + + // 4. Chat Completion + const completionUrl = "https://www.codebuff.com/api/v1/chat/completions"; + const response = await fetch(completionUrl, { + method: "POST", + headers: completionHeaders, + body: JSON.stringify(upstreamBody), + signal, + }); + + // 5. Finish agent run (background) + if (runId) { + void fetch("https://www.codebuff.com/api/v1/agent-runs", { + method: "POST", + headers: authHeaders, + body: JSON.stringify({ + action: "FINISH", + runId, + status: "completed", + totalSteps: 1, + directCredits: 0, + totalCredits: 0, + }), + }).catch(() => {}); + } + + return { response }; + } +} diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 8678fb6105..34f3bafe73 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -14,6 +14,7 @@ import { BedrockExecutor } from "./bedrock.ts"; import { GlmExecutor } from "./glm.ts"; import { PollinationsExecutor } from "./pollinations.ts"; import { CloudflareAIExecutor } from "./cloudflare-ai.ts"; +import { FreebuffExecutor } from "./freebuff.ts"; import { OpencodeExecutor } from "./opencode.ts"; import { VertexExecutor } from "./vertex.ts"; import { CliproxyapiExecutor } from "./cliproxyapi.ts"; @@ -117,6 +118,8 @@ const executors = { pol: new PollinationsExecutor(), // Alias "cloudflare-ai": new CloudflareAIExecutor(), cf: new CloudflareAIExecutor(), // Alias + freebuff: new FreebuffExecutor(), + fb: new FreebuffExecutor(), // Alias "opencode-zen": new OpencodeExecutor("opencode-zen"), "opencode-go": new OpencodeExecutor("opencode-go"), opencode: new OpencodeExecutor("opencode-zen"), // Alias for opencode-zen diff --git a/public/providers/freebuff-dark.svg b/public/providers/freebuff-dark.svg new file mode 100644 index 0000000000..0a6d1156fb --- /dev/null +++ b/public/providers/freebuff-dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/providers/freebuff-light.svg b/public/providers/freebuff-light.svg new file mode 100644 index 0000000000..0a6d1156fb --- /dev/null +++ b/public/providers/freebuff-light.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/providers/freebuff.png b/public/providers/freebuff.png new file mode 100644 index 0000000000000000000000000000000000000000..54806e0831485ec96658a790d63c2b4f312f2c80 GIT binary patch literal 5969 zcmeHL`BxL!w%%0-}@8Z`{AwCwbof@*IuXURGqWW{=WTP z@bYwDCA&@*09NhT?y?&I#Z45*(s(zU&@RL~ndt3%V*unr-?Bxbj}GnBp2 z{EXia!<{^w04i8=%VAOgYnpesIQb+X6a6AzmA%y@UTTDx(?=e#LM>1VnoEw=RnMel z(k?<8;|H{d+(16IK|PY#*W;lUl^Xl5HT3RL>f3z>E6+T>-f6jXa(=Ne_dibl{{E?> zRlPxpmlJ~|H_n`n{^WSv3m8>xxr3peWuLn^dJEo(gWcV0uVi%_s7A;p1&$1#ws@lO zI_?<>%MSW&+>37#VttDdVCWYNhtrc)7X4e|yu^0TNto$6tFErc)+xLKpWoBLeC=mR zf6)D=xHb}2zt*zr)2MLwi{GC45omh^3fa9&|8nRdVaA?0nJoeh=Hm3bwQTG8Xw_ew5s4hQ$7xdhf z@Yk^YqH>C`hpzR}w}3IA1Geisk-DItymA$G?uspfuF^M{1iO!0x*!3EsqdsEq_Zff<6 zEwFm%WbpYUx<~KR0KG)9f-JNitf26D_4MX1P-g%%oxWZvfeGyT^*R3iLf(KWH%SX_ zGhyu7U=38qBfI8B5@VnZE>F#FNS%s@5H(=4B6H;vstJm>B~G7HnwpxXJJwxvlLGAr zwY7Hjll7A+8FvW$2ci}a7#n&C|U}`s!-yH57x-rp_AA z0sodyD+RX_CNZK8CyyQNCF{?lu^m&iGFD)Z@eaIog z&LX}+rQ`zesekBdf0UyQw%a_m#4}EW-v?ei|DKN<39$O2iYng@#A_KDD|vmij|PCP zDlGle*BT#pFVq*IkpQf={p~$|E%1s00fo-u`r{aIg)#t_F0Ev||3`+%n}@i&yMOE` z@vnA_kB|5Bq|@b~e zykX1~!JZttI2#90;{lAaQ|Cj-|LvSK6$j47K>Od`7*S$kq7)XX4A&z<{*5nZYcL91 zTvJ%V@bK%@lp56R0LGnP_ANiGBBNeZ1iP_j?@0K13e8#unjgIiqpm^G*vr6uTJV(f zYD_%2?JSH*BUqw|?E~iFTNH>>hR0pwUH7}bRYJQH@IqMKL*aH5+5#Dt^?HP?y?u8H z(CUB!=hWBEdRMjYP%i}p8>~AubI1OwDbT9P(5E~hxtlqfue=W4Ya>`-Ue9amRnTgJ zLiMs}Ven(oE&{~<0^Ex)%fuX`JBX$%v2_&+)&m@7=vAdg$wY6p6o!XEUqGW~eK2OBh>#vt%zBWzSTFA)eHAA9}RUl+oO;Nu3{^<}^5x$?>>ifmJqw)w=5 zgI4>#oiN3U06R^F3&(2anAiTtzL}pNRSE7;VFYY>l&0*R5Mgt}?2UBvE_3d@49GOp zF+E(ZpQjPdLe)+fd!qj+-!aLsa`96>)7}Ue8AMDw1JyWR;k%NosVnAHIk3(MrMwclnW&9< zD1mK3@R-_$p-f$V!*A|EH3&4V21B(N(TUw5nV5sql&}uVr!x0xK>0?5z1v%G`;}`& z$!G8S$~4dIb=G&JAkPg7y;Lfz`lo)X>+U&%;Bh%k#D3hx612onvnaq%K)F{v)Wqu3 zM$7b**5m{;sz) zV*&5wE64QYI=|WREn3p1W@azfQ=W4h8yf}aD;fkit1Rfxn}yV=52`LtUf|qM_`+Bk zc{V2RLKe(V^ml7un!>j0NfLSUho{=LnG^!Ta4ldoB=h6x z2^!@6QwhD_v$ZV>Si{WWsh!VAo0Xw+B$AS5uBbp%maf{AF7EE`_E?anY(W^??qqms zNDtJV+7ob}yx)6vLz;mQBnLHkYh2c+1AlpOX4?CK#pZ*RLk`Icll=mRtPQ!UOstoP znWR7030&1nVQ)7v8+d_V)2ebmv`i5C=uqfFOsR^NL0M{3gH6}Wo-#pB84R~LoVB#% zmnU!v`0k|d{bmT|>ML>f{+`nHN87jA}HAeu|Bor zvbSOf$0F9xda)JNFJ5`?t*&%FUZMe=9qZ!C6ekT3V5@8}m2N-~s{RSRS-) ztJuaidWWP_x$$IhP|d{xc7#3*;4k3W1Lq=v6qpUr%-b`w<{4REdxTJ~h0@+OPGS!q zKHQx{)|)7B1uHsir^vS#9r<1BJjbCHun9#_KK!XC!p7HZbSWY<2QH<+B^Q(P)_%)& z18R&VBIZ&sSKqQ{e*eH=Iuo-c!(R=H(5s%}{=s@m0mlQ%jrlOUoX*O3GKkO<&y;tN zp+{VgHX*5y1((|I+(93Sk(H#XdVX={)7yjnM;$LH;xR#~urW-?4aUf8J(mcDm*G-? ziX<)`srlyOjAatBk&L-hK*}7S z2*-$kSj6wyYbF+{2u1N0`>B+01j8i!76cEar_j3iw;bOuH z)R=^ahkq=(x)=ghJArrg;3&UELRi5o6Z-LCrbrmKfv&c)>yW&jMTUS&zf#9 zivEp$%cB*w$kl!a;Ev)DHg53_=r=Y4qm~GhbJzDE4gwN85w|N3d$T$vjg77dI}pEB z4Mfzs%CVM0+T*CaFZeuVB*f~TvgqKKBOwN$P3i`*i z1d;`R(~A|1w~SiP+oQdXNYfJQo4>5njY1d(D{yi@sp%=Hx|YuGz?A?988bagTa4nSt(pF5iEJ9ZKOI}hMFEtC?oIEd%zi&QdY41hj#R>f>ZrjiGpwu)0qh(R zVEcBQQ2t~5vk3o0!vDr8&_}t205>v=zrc6Wc;TP)sJ1rn=zd(Lvcl^*s46Q4uRvQA z^DVmfXRu!-*$aSu|LAw%tAxT>&bo*|A{P3GE>1_1_Ixm{mXwqc%B!pWflW8m>=Os} zJJD=$b<5*?YHz3#nC^6q1^g-5yI@)(2TMy!QXu^Od5@K&tf0VWqb=Y?6z-w8NmS#j z$H(EEb<}_gjHND24PKMI7(diiQc}Xq3G^+Ql2=e@inhUPVP6ZOWN{|EsKxVkzqF0v zl|{ky0R%I(qf*UO*U?4Q2+zG}SMxP8g3A|=#70TYA^~u%z^78NJPyxl<9&ECRjC}u zS-;Owb&ih~IM<~(0|a}YnpTjMF5+5PZG|Ggcz=t=X;o_Rif|BA8Pd`|=R5Jb!E>o# z@%^b7JAGA+i*CdH$O#g*uVK-$BC?clw_E;XeEf=P@B*v9^18Cuz|+(7Xm9{R)5V#u zUgJ?omgYf8Ws*fdl9s=m`1w*sM#l7Bgp({<$EmNY(>zHUl7|vyh2jjO^fP^U>1bgp z);g2i(sYMl9zrPJB5Knky1-cDy98b0BPWK?5}LR4hegUU>6zI>k~&d~&Pm6$r70mq zEKl?Y!hPUKmyAY|D2AgPDSZ7ud5{?G;zOsE7&ljGCVdM-FlW|HPc;euYo3|V3oys6 zwM!|3DDAT1&#r0!!u>Wy6V>)P(~UI%tqS1_Nzvh*{wF{DM8hlCoXe8QY&DS2?mdM- z&6iPLe(fO-(|CpHwC0)r8gu6ub&KF#4QxBtsf=vQP!6)VM<0Cn*g-h+L+Q1>;)SeAvH-bk@ki ztwjR8DR+|$0v!VLq7J#ehh-Vra{yks8FvJnGtJhz-xa8?mO!@|%=U;M$K7LvCtI_(eRxgyXR7Eq=$WoKxFYk`VGr0lOwvwR8! zlqNiW`{Ace13%vTNtRc3=tti1n8eUsGQizjz~dw~%P=$c?F91WIAl{}Q3~)?UW^JO zd`%>^epXV^T|fOu>6B{Nb`BT80$l9AP=oxZ6(YuAS57P;D`(sTfkTVC%Q7T|_qyFs zT8Rvr*fMdz!UW*m&mVsEU=MYjgsPd7lJ29HM- zQB=#^9|l6-+^d1X@Hk?#hvF>;C+H+;UKZNFPZt#isV(bVUUFTvLA1A60TyoTTH8=* z7bkf;Qg65T;B!~06M}WS#W;2jFt3cC9n9jJSyvp7>JaN(7oSvgzIY1X36MV+u;Y-2 a2N>cu8E-4sfA(Bn>2|n!x|BIH&i)rPBP$L7 literal 0 HcmV?d00001 diff --git a/public/providers/freebuff.svg b/public/providers/freebuff.svg new file mode 100644 index 0000000000..0a6d1156fb --- /dev/null +++ b/public/providers/freebuff.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index de377cd9cb..8bdddc2aa7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -88,6 +88,7 @@ export default function AddApiKeyModal({ const isModal = provider === "modal"; const isGlm = isGlmProvider(provider); const isQoder = provider === "qoder"; + const isFreebuff = provider === "freebuff"; const openRouterPreset = useOpenRouterPresetControl(provider, t); const isCloudflare = provider === "cloudflare-ai"; const localProviderMetadata = getLocalProviderMetadata(provider); @@ -191,9 +192,11 @@ export default function AddApiKeyModal({ ? webSessionCredential.placeholder : isQoder ? t("qoderPatPlaceholder") - : apiKeyOptional - ? t("optional") - : undefined; + : isFreebuff + ? "Enter Freebuff / Codebuff Auth Token (e.g. 038fcdf9-...)" + : apiKeyOptional + ? t("optional") + : undefined; const apiCredentialHint = isModal ? providerText( t, @@ -202,8 +205,10 @@ export default function AddApiKeyModal({ ) : isQoder ? t("qoderPatHint") - : isWebSessionCredential - ? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false) + : isFreebuff + ? "Freebuff uses an authentic CLI auth token obtained via codebuff CLI login or automated harvester." + : isWebSessionCredential + ? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false) : isLocalSelfHostedProvider ? t("localProviderApiKeyOptionalHint", { provider: localProviderMetadata?.name || providerName || provider || "", diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index bb970b7af5..7f8180dee0 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -140,6 +140,37 @@ export { validateWebCookieProvider, bytezValidationResultFromStatus }; // validateKiroApiKeyRuntimeProbe now live in ./validation/webCookie and ./validation/kiro. // They are re-exported above to preserve the historical public surface. +export async function validateFreebuffProvider({ apiKey }: { apiKey: string }) { + if (!apiKey) { + return { valid: false, error: "Freebuff Auth Token required", unsupported: false }; + } + try { + const res = await fetch("https://www.codebuff.com/api/v1/freebuff/session", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "User-Agent": "codebuff/0.1.0 (darwin-arm64)", + "x-freebuff-model": "deepseek/deepseek-v4-flash", + }, + body: JSON.stringify({}), + signal: AbortSignal.timeout(15000), + }); + + if (res.ok || res.status === 409) { + return { valid: true, error: null }; + } + if (res.status === 401 || res.status === 403) { + return { valid: false, error: "Invalid or expired Freebuff Auth Token", unsupported: false }; + } + const errText = await res.text().catch(() => ""); + return { valid: false, error: `Freebuff validation returned ${res.status}: ${errText.slice(0, 100)}`, unsupported: false }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + return { valid: false, error: `Freebuff validation network error: ${msg}`, unsupported: false }; + } +} + export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) { provider = typeof provider === "string" ? resolveProviderId(provider) : provider; const requiresApiKey = !providerAllowsOptionalApiKey(provider); @@ -196,6 +227,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi firefly: validateAdobeFireflyProvider, qoder: validateQoderProvider, kiro: validateKiroProvider, + freebuff: validateFreebuffProvider, "command-code": validateCommandCodeProvider, huggingface: validateHuggingFaceProvider, // #5422: auth-only probe — Bytez 404s on every chat model until the account adds it to diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index 9e3524561a..aeb6f777ba 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -115,6 +115,7 @@ const KNOWN_SVGS = new Set([ "fal", "fireworks", "freeaiapikey", + "freebuff", "freemodel-dev", "friendli", "galadriel", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 1f2bd59979..606d6eb493 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -19,6 +19,21 @@ export const APIKEY_PROVIDERS_GATEWAYS = { "Create an API key at https://cheaperinference.com/?utm_source=omniroute (needs the `inference` scope), then paste the ir_live_… token here.", passthroughModels: true, }, + freebuff: { + id: "freebuff", + alias: "freebuff", + name: "Freebuff", + icon: "terminal", + color: "#10B981", + textIcon: "FB", + website: "https://freebuff.com", + hasFree: true, + serviceKinds: ["llm"], + authHint: "Enter Freebuff / Codebuff Auth Token (obtained via CLI login or automated harvester).", + freeNote: "Free Codebuff / Freebuff AI models.", + apiHint: "Token is authenticated against Codebuff upstream session pool.", + passthroughModels: true, + }, "charm-hyper": { id: "charm-hyper", alias: "charm-hyper", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 4fc9db6fdd..1878b2ac41 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -2164,6 +2164,29 @@ "stream": "https://freeinference.org/v1/chat/completions" } }, + "freebuff": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://www.codebuff.com/api/v1", + "stream": "https://www.codebuff.com/api/v1" + } + }, "freemodel-dev": { "format": "openai", "headers": { diff --git a/tests/unit/freebuff-provider.test.ts b/tests/unit/freebuff-provider.test.ts new file mode 100644 index 0000000000..d67de75f97 --- /dev/null +++ b/tests/unit/freebuff-provider.test.ts @@ -0,0 +1,59 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { FreebuffExecutor } from "../../open-sse/executors/freebuff.ts"; +import type { ExecuteInput } from "../../open-sse/executors/base.ts"; +import { freebuffProvider } from "../../open-sse/config/providers/registry/freebuff/index.ts"; +import { APIKEY_PROVIDERS_GATEWAYS } from "../../src/shared/constants/providers/apikey/gateways.ts"; +import { validateFreebuffProvider } from "../../src/lib/providers/validation.ts"; + +test("FreebuffExecutor: constructor initializes provider name correctly", () => { + const executor = new FreebuffExecutor(); + assert.equal(executor.getProvider(), "freebuff"); +}); + +test("FreebuffExecutor: returns 401 response when credentials are missing", async () => { + const executor = new FreebuffExecutor(); + const res = await executor.execute({ + model: "deepseek/deepseek-v4-flash", + body: { messages: [{ role: "user", content: "hello" }] }, + stream: false, + credentials: { apiKey: "" }, + } as unknown as ExecuteInput); + + assert.equal(res.response.status, 401); + const data = (await res.response.json()) as { error: { message: string } }; + assert.match(data.error.message, /Freebuff Auth Token required/i); +}); + +test("freebuffProvider: registry entry has valid structure and catalog", () => { + assert.equal(freebuffProvider.id, "freebuff"); + assert.equal(freebuffProvider.format, "openai"); + assert.equal(freebuffProvider.executor, "freebuff"); + assert.equal(freebuffProvider.baseUrl, "https://www.codebuff.com/api/v1"); + assert.ok(Array.isArray(freebuffProvider.models)); + assert.ok(freebuffProvider.models.length >= 8); + + const flash = freebuffProvider.models.find((m) => m.id === "deepseek/deepseek-v4-flash"); + assert.ok(flash, "deepseek/deepseek-v4-flash must exist in freebuff models"); + assert.equal(flash?.supportsReasoning, true); + + const minimax = freebuffProvider.models.find((m) => m.id === "minimax/minimax-m3"); + assert.ok(minimax, "minimax/minimax-m3 must exist in freebuff models"); + assert.equal(minimax?.supportsVision, true); +}); + +test("APIKEY_PROVIDERS_GATEWAYS: freebuff gateway metadata is defined", () => { + const fb = APIKEY_PROVIDERS_GATEWAYS.freebuff; + assert.ok(fb, "freebuff must be in APIKEY_PROVIDERS_GATEWAYS"); + assert.equal(fb.id, "freebuff"); + assert.equal(fb.name, "Freebuff"); + assert.equal(fb.color, "#10B981"); + assert.equal(fb.hasFree, true); +}); + +test("validateFreebuffProvider: returns invalid when apiKey is empty", async () => { + const res = await validateFreebuffProvider({ apiKey: "" }); + assert.equal(res.valid, false); + assert.match(res.error || "", /Freebuff Auth Token required/i); +}); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index abc46e6870..9276da2206 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -23,7 +23,8 @@ // gateways family to 228 measured on the tip; Puter retired (#10210) and chatanywhere restored // (base-reds round 3, #9985) are both included in that measurement; Cursor API (specialty-media, // #10729) brings it to 229; Token Kiosk (gateways, #10722) — merged in the same -// merge-train batch — independently bumped the gateways family too, landing at 231. +// merge-train batch — independently bumped the gateways family too, landing at 231; Freebuff +// (gateways, #10531) brings it to 232. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -52,12 +53,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 232 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 231); - assert.equal(new Set(keys).size, 231, "duplicate keys after spread-merge"); + assert.equal(keys.length, 232); + assert.equal(new Set(keys).size, 232, "duplicate keys after spread-merge"); // the merged object's entry-count equals the sum of the 6 semantic family files; families are a - // strict partition (every provider in exactly one), so the sum must be exactly 231. + // strict partition (every provider in exactly one), so the sum must be exactly 232. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -77,7 +78,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 231, "families must partition all 231 providers"); + assert.equal(famTotal, 232, "families must partition all 232 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {