From 142ae93498fd5bf90a5eb6fdc3c2892477630635 Mon Sep 17 00:00:00 2001 From: excessivechaos Date: Mon, 17 Aug 2026 07:19:26 -0700 Subject: [PATCH 01/71] 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 d87b97a78635725139fbff234905d1819c1db268 Mon Sep 17 00:00:00 2001 From: 3g0r1ch Date: Thu, 20 Aug 2026 23:28:30 +0300 Subject: [PATCH 02/71] =?UTF-8?q?feat(routing):=20adaptive=20feedback=20lo?= =?UTF-8?q?op=20v2=20=E2=80=94=20operational/semantic=20quality,=20confide?= =?UTF-8?q?nce,=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 03/71] 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 04/71] 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 05/71] 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 06/71] 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 07/71] 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 08/71] 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 09/71] 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 10/71] 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 11/71] 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 12/71] 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 13/71] 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 14/71] 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 15/71] 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 16/71] 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 17/71] 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 18/71] 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 19/71] 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 20/71] 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 21/71] 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 22/71] 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 23/71] 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 24/71] 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 25/71] 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 26/71] 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 27/71] 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 28/71] 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", () => { From d098114fe559e51bc6c6a8feacc993775f23bf31 Mon Sep 17 00:00:00 2001 From: Hernan Javier Ardila Sanchez Date: Fri, 21 Aug 2026 04:07:25 +0200 Subject: [PATCH 29/71] fix(combo): guarantee combo loops terminate with an actionable error instead of hanging silently (#10463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guarantees combo/target loops always terminate with an actionable error instead of hanging when an upstream hangs — fixes 10 silent-stop gaps (G1–G10): combo-loop safety timer + abort on hang → 504, unexpected task throw → 502, round-robin safety net, chaos all-panel-failure visibility, ReDoS-safe eval regex, autoRefreshDaemon swallowed-error logging, batch-item wall-clock timeout. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (2 real conflicts in combo.ts and chaosEngine.ts, both additive features from concurrently-merged PRs landing at the same insertion point — resolved by combining both, verified no variable-shadowing/scoping issues): - 10/10 new regression tests pass (combo-silent-stop-gaps.test.ts): hung upstream → 504, unexpected throw → 502, chaos all-fail logged, ReDoS regex rejected, batch timeout. - 163/163 broader focused tests pass (combo-routing-engine, combo-target-timeout-runner, pipeline-router, chaos-executor, batch-processor ×2, service-batch-processor, evalrunner-builtinsuites-split). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: herjarsa --- open-sse/services/autoCombo/chaosEngine.ts | 30 +- open-sse/services/autoCombo/pipelineRouter.ts | 11 + open-sse/services/autoRefreshDaemon.ts | 21 +- open-sse/services/batchProcessor.ts | 44 ++- open-sse/services/combo.ts | 169 +++++++++- open-sse/services/combo/comboPredicates.ts | 9 + .../services/combo/targetTimeoutRunner.ts | 7 + src/lib/evals/evalRunner.ts | 11 + tests/unit/combo-silent-stop-gaps.test.ts | 305 ++++++++++++++++++ 9 files changed, 584 insertions(+), 23 deletions(-) create mode 100644 tests/unit/combo-silent-stop-gaps.test.ts diff --git a/open-sse/services/autoCombo/chaosEngine.ts b/open-sse/services/autoCombo/chaosEngine.ts index 89813fe48f..32f5b09f48 100644 --- a/open-sse/services/autoCombo/chaosEngine.ts +++ b/open-sse/services/autoCombo/chaosEngine.ts @@ -180,7 +180,22 @@ function dispatchOnePanelModel(opts: { log?.info?.( `CHAOS panel ${index} (${model}) ok=${res.ok} status=${res.status} textLen=${text.length}` ); - const part: ChaosPart = { model, index, ok: true, text }; + // G5b: honor the upstream response status — a 4xx/5xx is a panel FAILURE, + // not a success (previously ok:true was hardcoded, so an all-error panel + // never reached the all-failed branch and the error text was streamed as + // if it were a successful answer). + if (res.ok) { + const part: ChaosPart = { model, index, ok: true, text }; + await onResult?.(part); + return part; + } + const part: ChaosPart = { + model, + index, + ok: false, + text: "", + error: `upstream ${res.status}: ${text.slice(0, 200) || res.statusText || "error"}`, + }; await onResult?.(part); return part; } catch (err) { @@ -466,8 +481,17 @@ export async function handleChaosChat(opts: { } if (successes.length === 0) { - const errText = "All chaos panel models failed"; - await safeEnqueue(chatChunk(chunkId, panelToDispatch[0] ?? "", errText)); + // G5 (silent-stop fix): make an all-panel failure visible server-side. + // The status stays 200 (SSE envelope must stay well-formed), but the + // failure is now logged with the per-model errors so operators can see + // why the chaos panel produced nothing. + const modelErrors = allParts.map((p) => `${p.model}: ${p.error ?? "unknown"}`).join(" | "); + log?.warn?.( + "CHAOS", + `All chaos panel models failed for ${comboName ?? "panel"}: ${modelErrors}` + ); + const errText = `All chaos panel models failed — ${modelErrors}`; + await safeEnqueue(chatChunk(chunkId, panelToDispatch[0] ?? panel[0] ?? "", errText)); await safeEnqueue(SSE_DONE); await enqueueChain; closed = true; diff --git a/open-sse/services/autoCombo/pipelineRouter.ts b/open-sse/services/autoCombo/pipelineRouter.ts index 5fbc9eea02..bc2ce3c5d5 100644 --- a/open-sse/services/autoCombo/pipelineRouter.ts +++ b/open-sse/services/autoCombo/pipelineRouter.ts @@ -343,6 +343,17 @@ export async function handlePipelineCombo({ } } + // G6 (silent-stop fix): if the reflection loop burned its retry budget and the + // verdict is still "fail", the fall-through below returns a FAILED result + // indistinguishable from a first-attempt failure. Surface it loudly so the + // caller (and operator logs) can tell "retries exhausted" apart. + if (result.reflectVerdict === "fail" && reflectionCount > 0) { + log.warn( + "PIPELINE", + `Reflection retries exhausted (${reflectionCount}/${maxReflectionLoops}) — pipeline verdict still "fail", returning the original failed result` + ); + } + // ── Return result ───────────────────────────────────────────────────────── // Check if the last stage has a streaming Response const lastStage = result.stages[result.stages.length - 1]; diff --git a/open-sse/services/autoRefreshDaemon.ts b/open-sse/services/autoRefreshDaemon.ts index 120b081545..3a177a87ae 100644 --- a/open-sse/services/autoRefreshDaemon.ts +++ b/open-sse/services/autoRefreshDaemon.ts @@ -125,8 +125,13 @@ class AutoRefreshDaemon { `[AutoRefreshDaemon] Credential expired for "${providerId}" (${config.displayName})` ); } - } catch { - // Network errors are non-fatal — retry next cycle + } catch (err) { + // Network errors are non-fatal — retry next cycle. G8: log which + // provider failed so credential problems are not silently masked. + console.warn( + `[AutoRefreshDaemon] Network error validating credential for "${providerId}" — retry next cycle`, + err instanceof Error ? err.message : err + ); } } @@ -165,8 +170,16 @@ class AutoRefreshDaemon { } return true; - } catch { - // Network errors (timeout, DNS failure) don't mean the credential is bad + } catch (err) { + // Network errors (timeout, DNS failure) don't mean the credential is bad. + // G8 (silent-stop fix): the previous bare `catch { return true; }` swallowed + // the error entirely — operators could never tell a credential was failing + // to validate due to network trouble. Log it (provider + reason) before + // returning the fail-open result. + console.warn( + `[AutoRefreshDaemon] Network error validating credential for "${providerId}" — treated as valid (fail-open), will retry next cycle`, + err instanceof Error ? err.message : err + ); return true; } finally { clearTimeout(timeout); diff --git a/open-sse/services/batchProcessor.ts b/open-sse/services/batchProcessor.ts index 578814427a..e9fe915afd 100644 --- a/open-sse/services/batchProcessor.ts +++ b/open-sse/services/batchProcessor.ts @@ -506,14 +506,46 @@ async function processSingleItemWithRetry(item: BatchRequestItem, apiKey: string } } +// G10 (silent-stop fix): individual batch-item dispatches can hang indefinitely +// if the upstream route stalls (no signal/timeout plumbed through). Bound each +// item with a wall-clock timeout so a stuck item fails fast (recorded as an item +// error) instead of freezing the whole batch loop. The orphaned dispatch keeps +// running in the background but can no longer block the batch. +export const BATCH_ITEM_DISPATCH_TIMEOUT_MS = 120_000; + +/** + * G10: race a promise against a wall-clock deadline. Exported for unit testing + * (batch dispatch is a module-internal import, so the timeout mechanism itself + * is verified directly here). + */ +export function withItemDispatchTimeout( + promise: Promise, + timeoutMs: number, + label: string +): Promise { + let timer: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), + timeoutMs + ); + }); + return Promise.race([promise, timeoutPromise]).finally(() => { + if (timer) clearTimeout(timer); + }); +} + async function processSingleItem(item: BatchRequestItem, apiKey: string) { const body = buildRequestBody(item); - - return await dispatch.dispatchBatchApiRequest({ - endpoint: item.url, - body, - apiKey, - }); + return withItemDispatchTimeout( + dispatch.dispatchBatchApiRequest({ + endpoint: item.url, + body, + apiKey, + }), + BATCH_ITEM_DISPATCH_TIMEOUT_MS, + `Batch item dispatch (${item.url})` + ); } export function buildRequestBody(item: BatchRequestItem) { diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 9b4069bbc0..6a1d7cbbf2 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -179,6 +179,8 @@ import { TRANSIENT_FOR_SEMAPHORE, MAX_FALLBACK_WAIT_MS, MAX_GLOBAL_ATTEMPTS, + COMBO_LOOP_SAFETY_TIMEOUT_MS, + COMBO_SAFETY_DRAIN_MS, isAllAccountsRateLimitedResponse, clampComboDepth, shouldSkipForPredictedTtft, @@ -1046,11 +1048,54 @@ async function handleComboChatInner({ const globalPromise = new Promise((res) => { globalResolve = res; }); + + // G1 (silent-stop fix): the speculative loop's `Promise.race` waits on + // `globalPromise`, which is ONLY resolved from inside a task (success or + // fatal error). If a target hangs — e.g. the operator disabled the per-model + // timeout (`targetTimeoutMs: 0`) and the upstream never settles — the race + // never resolves and the request hangs forever with no response. This safety + // promise force-resolves after the combo budget (comboTimeoutMs when set, + // otherwise a hard ceiling) so the request ALWAYS terminates with an + // actionable 504 instead of dying silently. `comboExpired` is flipped so the + // target loop stops launching new work; the existing comboExpired branch + // returns the aggregated 504. + const loopSafetyMs = + comboTimeoutMs > 0 ? comboTimeoutMs : COMBO_LOOP_SAFETY_TIMEOUT_MS; + let loopSafetyFired = false; + let loopSafetyTimer: ReturnType | null = null; + const loopSafetyPromise = new Promise((resolve) => { + loopSafetyTimer = setTimeout(() => { + loopSafetyFired = true; + log.warn( + "COMBO", + `Combo loop safety timeout (${loopSafetyMs}ms) reached without a terminal response — force-terminating` + ); + resolve( + errorResponseWithComboDiagnostics( + 504, + `Combo global timeout (${loopSafetyMs}ms) without a terminal response`, + buildComboDiag("combo_timeout"), + { code: "COMBO_TIMEOUT", type: "server_error" } + ) + ); + }, loopSafetyMs); + loopSafetyTimer.unref?.(); + }); const runningTasks = new Set>(); let anySuccess = false; // #10681: steps already recorded as dispatched (so per-target retries do not // duplicate the decision). const dispatchedTargets = new Set(); + // G1: flip comboExpired as soon as the safety timer fires so the next loop + // iteration breaks instead of launching more targets after the budget, and + // abort every in-flight target so a hung upstream actually gets cancelled + // (not just "response stops"). + const markLoopExpiredIfSafetyFired = () => { + if (loopSafetyFired) { + comboExpired = true; + for (const [, ac] of abortControllers.entries()) ac.abort(); + } + }; const abortControllers = new Map(); const zeroLatencyOptimizationsEnabled = config.zeroLatencyOptimizationsEnabled === true; const hasProtectedPriorityTarget = @@ -2363,6 +2408,17 @@ async function handleComboChatInner({ })().catch((err) => { const logError = log.error ?? log.warn; logError("COMBO", `Speculative task error for target ${i}`, err); + // G2 (silent-stop fix): never leave the speculative loop waiting on an + // unresolved globalPromise. If a task throws unexpectedly (outside + // executeTarget's error handling) and no other task succeeds, the post-loop + // `Promise.race([globalPromise, ...])` would hang forever. Resolve with a + // 502 so the request terminates with an actionable error. + if (!anySuccess && globalResolve) { + anySuccess = true; + globalResolve( + errorResponse(502, `Combo target ${i} failed with an unexpected error`) + ); + } }); runningTasks.add(task); @@ -2380,10 +2436,11 @@ async function handleComboChatInner({ timeoutResolve = r; setTimeout(r, hedgeDelay); }); - await Promise.race([task, globalPromise, timeoutPromise]); + await Promise.race([task, globalPromise, timeoutPromise, loopSafetyPromise]); } else { - await Promise.race([task, globalPromise]); + await Promise.race([task, globalPromise, loopSafetyPromise]); } + markLoopExpiredIfSafetyFired(); // Global combo timeout check: after each target completes, stop trying // further targets if the total elapsed time exceeds comboTimeoutMs. @@ -2398,13 +2455,51 @@ async function handleComboChatInner({ } if (!anySuccess && runningTasks.size > 0) { - await Promise.race([globalPromise, Promise.all([...runningTasks])]); + // G1: include loopSafetyPromise so a hung last task (per-model timeout + // disabled) cannot freeze this post-loop race forever. + await Promise.race([globalPromise, Promise.all([...runningTasks]), loopSafetyPromise]); + markLoopExpiredIfSafetyFired(); + } + + // G1: if the safety timer won the race (request would otherwise hang), give + // in-flight tasks a short drain window to land their per-model errors into + // comboErrors so the 504 carries the same "tried: a (500)" summary the + // regular comboExpired branch produces — then return the safety 504. + if (loopSafetyFired && !anySuccess) { + if (runningTasks.size > 0) { + await Promise.race([ + Promise.allSettled([...runningTasks]), + new Promise((resolve) => setTimeout(resolve, COMBO_SAFETY_DRAIN_MS)), + ]); + } + const summary = comboErrors + .slice(0, 5) + .map((e) => `${e.model} (${e.status})`) + .join(", "); + const msg = + `Combo global timeout (${loopSafetyMs}ms) after ${recordedAttempts}/${orderedTargets.length} targets` + + (comboErrors.length > 0 + ? ` | tried: ${summary}${comboErrors.length > 5 ? `... (+${comboErrors.length - 5})` : ""}` + : "") + + " without a terminal response"; + return errorResponseWithComboDiagnostics( + 504, + msg, + buildComboDiag("combo_timeout"), + { code: "COMBO_TIMEOUT", type: "server_error" } + ); } // #10681: finalize the decision trace (success). finalizeComboTrace(traceInvocationId, orderedTargets); finishComboTrace(traceInvocationId, { status: 200 }); if (anySuccess) { + // G1: clear the safety timer on the happy path so a successful combo does + // not leave a 10-minute timer alive per request. + if (loopSafetyTimer) { + clearTimeout(loopSafetyTimer); + loopSafetyTimer = null; + } return await globalPromise; } @@ -2923,6 +3018,33 @@ async function handleRoundRobinCombo({ // and the "Done with this model" path below), mirroring handleComboChat. const rrOutcomes: Array = []; + // G4 (silent-stop fix): round-robin has NO global timeout — a hung model + // (per-model timeout disabled via targetTimeoutMs: 0) would freeze the request + // forever with no response. Safety promise + timer bound the whole loop; when + // it fires, rrExpired flips and every subsequent model attempt short-circuits + // to the 504. Cleaned up in the loop's finally. + const rrConfiguredTimeoutMs = + (config as { comboTimeoutMs?: number }).comboTimeoutMs ?? 0; + const rrLoopSafetyMs = + rrConfiguredTimeoutMs > 0 ? rrConfiguredTimeoutMs : COMBO_LOOP_SAFETY_TIMEOUT_MS; + let rrExpired = false; + let rrLoopSafetyTimer: ReturnType | null = null; + let rrResolveSafety: ((res: Response) => void) | null = null; + const rrSafetyPromise = new Promise((resolve) => { + rrResolveSafety = resolve; + }); + rrLoopSafetyTimer = setTimeout(() => { + rrExpired = true; + log.warn( + "COMBO-RR", + `Round-robin loop exceeded ${rrLoopSafetyMs}ms without a terminal response — force-terminating` + ); + rrResolveSafety?.( + errorResponse(504, `Round-robin combo exceeded ${rrLoopSafetyMs}ms without a terminal response`) + ); + }, rrLoopSafetyMs); + rrLoopSafetyTimer.unref?.(); + // #1731: Per-request in-memory set of providers whose quota is fully exhausted. // When a target returns a quota-exhausted 429, remaining targets from the same // provider are skipped to avoid the cascade through N same-provider targets. @@ -2931,8 +3053,11 @@ async function handleRoundRobinCombo({ const transientRateLimitedProviders = new Set(); // Try each model starting from the round-robin target - for (let offset = 0; offset < modelCount; offset++) { - const modelIndex = (rrStartIndex + offset) % modelCount; + try { + for (let offset = 0; offset < modelCount; offset++) { + // G4: stop launching new work once the safety timer fired. + if (rrExpired) break; + const modelIndex = (rrStartIndex + offset) % modelCount; const target = filteredTargets[modelIndex]; const modelStr = target.modelStr; const provider = target.provider; @@ -3077,11 +3202,15 @@ async function handleRoundRobinCombo({ fingerprint: resolveTargetFingerprint(target) ?? "", }); - const result = await handleSingleModel(attemptBody, modelStr, { - ...targetForAttempt, - effectiveComboStrategy: "round-robin", - failoverBeforeRetry: config.failoverBeforeRetry, - }); + const result = await Promise.race([ + handleSingleModel(attemptBody, modelStr, { + ...targetForAttempt, + effectiveComboStrategy: "round-robin", + failoverBeforeRetry: config.failoverBeforeRetry, + }), + rrSafetyPromise, + ]); + if (rrExpired) return result; // G4: safety timer won — stop everything // Quota-aware scheduling: reserve the estimated budget for this // dispatch (opt-in, same env gate as the pre-request check). Best-effort @@ -3519,6 +3648,26 @@ async function handleRoundRobinCombo({ release(); } } + } catch (err) { + // G4: unexpected exception in the round-robin loop must never crash the + // request silently — surface a 500 instead of hanging the client. + log.error?.("COMBO-RR", "Unexpected error in round-robin loop", err); + return errorResponse(500, "Unexpected error in round-robin combo"); + } finally { + if (rrLoopSafetyTimer) { + clearTimeout(rrLoopSafetyTimer); + rrLoopSafetyTimer = null; + } + } + + // G4: if the safety timer fired between iterations (no race captured it), + // terminate with the actionable 504 instead of the generic exhaustion path. + if (rrExpired) { + return errorResponse( + 504, + `Round-robin combo exceeded ${rrLoopSafetyMs}ms without a terminal response` + ); + } // All models exhausted const latencyMs = Date.now() - startTime; diff --git a/open-sse/services/combo/comboPredicates.ts b/open-sse/services/combo/comboPredicates.ts index dc87509236..f7cfa3322d 100644 --- a/open-sse/services/combo/comboPredicates.ts +++ b/open-sse/services/combo/comboPredicates.ts @@ -18,6 +18,15 @@ import type { ResolvedComboTarget } from "./types.ts"; // Status codes that should mark round-robin target semaphores as cooling down. export const TRANSIENT_FOR_SEMAPHORE = [429, 502, 503, 504]; +// G1 (silent-stop fix): hard ceiling for the combo target loop when the operator +// left comboTimeoutMs at 0 ("unlimited"). Without this, a hung upstream (per-model +// timeout disabled) would freeze the request forever with no response. 10 minutes +// is a generous bound for legitimate long-running fallback cascades. +export const COMBO_LOOP_SAFETY_TIMEOUT_MS = 10 * 60 * 1000; +// G1: after the safety timer fires, wait this long for in-flight targets to land +// their per-model errors into comboErrors (so the 504 carries the same "tried:" +// summary as the regular timeout path) before returning the safety response. +export const COMBO_SAFETY_DRAIN_MS = 2000; // Patterns that signal all accounts for a provider are rate-limited / exhausted. // Used to detect 503 responses from handleNoCredentials so combo can fallback. export const ALL_ACCOUNTS_RATE_LIMITED_PATTERNS = [ diff --git a/open-sse/services/combo/targetTimeoutRunner.ts b/open-sse/services/combo/targetTimeoutRunner.ts index 4eb6cb8b68..fb5c2262f0 100644 --- a/open-sse/services/combo/targetTimeoutRunner.ts +++ b/open-sse/services/combo/targetTimeoutRunner.ts @@ -106,6 +106,13 @@ export function buildTargetTimeoutRunner(deps: { target?: SingleModelTarget ): Promise => { if (comboTargetTimeoutMs <= 0) { + // G3 (silent-stop fix): a disabled per-model timeout means a hung upstream + // stalls the target until the combo loop safety timer (COMBO_LOOP_SAFETY_TIMEOUT_MS) + // force-terminates — surface that dependency instead of silently running bare. + log.warn( + "COMBO", + `Per-model combo timeout is DISABLED (comboTargetTimeoutMs=${comboTargetTimeoutMs}) for ${modelStr} — a hung upstream will hang this target until the combo loop safety timeout` + ); return handleSingleModel(b, modelStr, target).catch((err) => errorResponse(502, err?.message ?? "Upstream model error") ); diff --git a/src/lib/evals/evalRunner.ts b/src/lib/evals/evalRunner.ts index d67ea9d74b..d0b9280013 100644 --- a/src/lib/evals/evalRunner.ts +++ b/src/lib/evals/evalRunner.ts @@ -9,6 +9,7 @@ */ import { getCustomEvalSuite, listCustomEvalSuites } from "@/lib/db/evals"; +import safeRegex from "safe-regex"; import { goldenSet, codingSuite, @@ -161,6 +162,16 @@ export function evaluateCase(evalCase: any, actualOutput: string) { details.error = "Regex pattern too large for safe evaluation."; break; } + // G7 (silent-stop fix): a catastrophic regex (nested quantifiers like + // `(a+)+$`) can hang the event loop for minutes on adversarial output — + // the eval loop then "stops doing anything" with no error. safe-regex + // statically rejects such patterns before test() runs. + if (!safeRegex(regex)) { + passed = false; + details.error = + "Regex pattern rejected as potentially unsafe (catastrophic backtracking risk). Simplify the pattern."; + break; + } passed = regex.test(actualOutput); details.pattern = String(expectedValue); break; diff --git a/tests/unit/combo-silent-stop-gaps.test.ts b/tests/unit/combo-silent-stop-gaps.test.ts new file mode 100644 index 0000000000..3b101a20c0 --- /dev/null +++ b/tests/unit/combo-silent-stop-gaps.test.ts @@ -0,0 +1,305 @@ +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"; + +// Isolate DATA_DIR before any DB-touching import (combo.ts pulls in the +// SQLite layer) — mirrors tests/unit/combo-routing-engine.test.ts. +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-silent-stop-gaps-")); +const ORIGINAL_DATA_DIR = process.env.DATA_DIR; +process.env.DATA_DIR = TEST_DATA_DIR; + +// --------------------------------------------------------------------------- +// Silent-stop gap fixes — regression tests +// --------------------------------------------------------------------------- +// These lock the G1–G10 fixes for loops that could terminate silently: +// G1 combo.ts globalPromise safety timer (hung target → 504, never hang) +// G2 combo.ts task wrapper catch resolves globalPromise (unexpected throw → 502) +// G3 targetTimeoutRunner warns when per-model timeout is disabled +// G4 round-robin loop safety timer (hung model → 504) +// G5 chaosEngine logs all-panel failures with per-model errors +// G7 evalRunner rejects catastrophic regex (ReDoS) via safe-regex +// G8 autoRefreshDaemon logs network errors per provider +// G10 batchProcessor item dispatch timeout (hung item fails fast) +// --------------------------------------------------------------------------- + +const { buildTargetTimeoutRunner } = + await import("../../open-sse/services/combo/targetTimeoutRunner.ts"); +const { handleComboChat } = await import("../../open-sse/services/combo.ts"); +const { handleChaosChat } = await import("../../open-sse/services/autoCombo/chaosEngine.ts"); +const { evaluateCase } = await import("../../src/lib/evals/evalRunner.ts"); +const { withItemDispatchTimeout } = await import("../../open-sse/services/batchProcessor.ts"); +const { saveModelsDevCapabilities } = await import("../../src/lib/modelsDevSync.ts"); + +function capabilityEntry(limitContext: unknown, 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: limitContext, + limit_input: limitContext, + limit_output: 4096, + interleaved_field: null, + ...overrides, + }; +} + +function createLog() { + const entries: Array<{ level: string; tag: string; msg: string }> = []; + return { + info: (tag: string, msg: string) => entries.push({ level: "info", tag, msg }), + warn: (tag: string, msg: string) => entries.push({ level: "warn", tag, msg }), + error: (tag: string, msg: string) => entries.push({ level: "error", tag, msg }), + debug: (tag: string, msg: string) => entries.push({ level: "debug", tag, msg }), + entries, + }; +} + +function okResponse(body: Record = { choices: [{ message: { content: "ok" } }] }) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function errorResponse(status: number, message: string = `Error ${status}`) { + return new Response(JSON.stringify({ error: { message } }), { + status, + headers: { "content-type": "application/json" }, + }); +} + +// ── G1: combo loop safety timer ───────────────────────────────────────────── +// A hung target (per-model timeout disabled / upstream never settles) must NOT +// freeze the request forever: the safety timer force-resolves with 504. +test.after(async () => { + if (ORIGINAL_DATA_DIR === undefined) { + delete process.env.DATA_DIR; + } else { + process.env.DATA_DIR = ORIGINAL_DATA_DIR; + } + try { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } catch { + /* best effort */ + } +}); + +test("G1: hung target with per-model timeout disabled → 504, not a silent hang", async () => { + const log = createLog(); + const combo = { + name: "g1-hang", + models: ["openai/gpt-4o-mini"], + config: { + // Per-model timeout off + tiny global budget so the test runs in ms. + targetTimeoutMs: 0, + comboTimeoutMs: 100, + }, + }; + saveModelsDevCapabilities({ openai: { "gpt-4o-mini": capabilityEntry(128000) } }); + const startedAt = Date.now(); + const result = await handleComboChat({ + body: {}, + combo, + // Never settles — simulates an upstream that accepts the connection and + // then stalls forever. + handleSingleModel: () => new Promise(() => {}), + isModelAvailable: async () => true, + log, + settings: null, + allCombos: null, + }); + const elapsed = Date.now() - startedAt; + assert.equal(result.status, 504); + assert.ok(elapsed < 10_000, `safety timeout took ${elapsed}ms — too slow`); + assert.ok( + log.entries.some((e) => e.level === "warn" && /safety timeout/i.test(String(e.msg))), + "expected a warn about the combo loop safety timeout" + ); +}); + +// ── G2: task wrapper catch resolves globalPromise ─────────────────────────── +// An unexpected throw inside executeTarget (e.g. isModelAvailable exploding) +// must surface as 502, not leave the request hanging. +test("G2: unexpected throw in a target surfaces 502 instead of hanging", async () => { + const log = createLog(); + const combo = { + name: "g2-throw", + models: ["openai/gpt-4o-mini"], + }; + const result = await handleComboChat({ + body: {}, + combo, + handleSingleModel: async () => okResponse(), + // Throw inside executeTarget before dispatch — the wrapper catch must + // resolve globalPromise so the loop terminates. + isModelAvailable: async () => { + throw new Error("unexpected availability explosion"); + }, + log, + settings: null, + allCombos: null, + }); + assert.equal(result.status, 502); + const body = await result.text(); + assert.match(body, /unexpected error/i); + assert.ok( + log.entries.some((e) => e.level === "error" && /Speculative task error/i.test(String(e.msg))), + "expected the speculative task error to be logged" + ); +}); + +// ── G3: targetTimeoutRunner warns when per-model timeout is disabled ──────── +test("G3: targetTimeoutRunner warns when comboTargetTimeoutMs <= 0", async () => { + const log = createLog(); + const runner = buildTargetTimeoutRunner({ + handleSingleModel: async () => new Response("ok"), + comboTargetTimeoutMs: 0, + log, + }); + const res = await runner({}, "m"); + assert.equal(await res.text(), "ok"); + assert.ok( + log.entries.some((e) => e.level === "warn" && /DISABLED|disabled/i.test(String(e.msg))), + "expected a warn about the disabled per-model timeout" + ); +}); + +// ── G4: round-robin loop safety timer ─────────────────────────────────────── +test("G4: round-robin hung model → 504 via loop safety timer", async () => { + const log = createLog(); + const combo = { + name: "g4-rr-hang", + models: ["openai/gpt-4o-mini", "claude/sonnet"], + strategy: "round-robin", + config: { + targetTimeoutMs: 0, + comboTimeoutMs: 100, + }, + }; + saveModelsDevCapabilities({ + openai: { "gpt-4o-mini": capabilityEntry(128000) }, + claude: { sonnet: capabilityEntry(200000) }, + }); + const startedAt = Date.now(); + const result = await handleComboChat({ + body: {}, + combo, + handleSingleModel: () => new Promise(() => {}), + isModelAvailable: async () => true, + log, + settings: null, + allCombos: null, + }); + const elapsed = Date.now() - startedAt; + assert.equal(result.status, 504); + assert.ok(elapsed < 10_000, `RR safety timeout took ${elapsed}ms — too slow`); + assert.ok( + log.entries.some((e) => e.level === "warn" && /Round-robin/i.test(String(e.msg))), + "expected a warn about the round-robin safety timeout" + ); +}); + +// ── G5: chaosEngine logs all-panel failures ───────────────────────────────── +test("G5: chaos all-panel failure is logged with per-model errors", async () => { + const log = createLog(); + const res = await handleChaosChat({ + body: {}, + models: ["openai/gpt-4o-mini", "claude/sonnet"], + handleSingleModel: async () => errorResponse(503, "upstream down"), + log, + comboName: "g5-chaos", + }); + assert.equal(res.status, 200); // SSE envelope stays well-formed + const body = await res.text(); + assert.match(body, /All chaos panel models failed/); + assert.match(body, /upstream down/); + assert.ok( + log.entries.some( + (e) => e.level === "warn" && /All chaos panel models failed/i.test(String(e.msg)) + ), + "expected the all-failed warn with model errors" + ); +}); + +// ── G7: evalRunner rejects catastrophic regex (ReDoS guard) ───────────────── +test("G7: catastrophic regex is rejected instead of hanging the eval loop", () => { + const startedAt = Date.now(); + const result = evaluateCase( + { id: "redos", name: "redos", expected: { strategy: "regex", value: "(a+)+$" } }, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" + ); + const elapsed = Date.now() - startedAt; + assert.equal(result.passed, false); + assert.match(String(result.details?.error ?? ""), /unsafe|backtracking/i); + assert.ok(elapsed < 1000, `regex eval took ${elapsed}ms — ReDoS guard failed`); +}); + +test("G7: benign regex still evaluates normally", () => { + const result = evaluateCase( + { id: "ok-regex", name: "ok-regex", expected: { strategy: "regex", value: "^hello" } }, + "hello world" + ); + assert.equal(result.passed, true); +}); + +// ── G8: autoRefreshDaemon logs network errors per provider ────────────────── +test("G8: autoRefreshDaemon logs network errors instead of swallowing them", async () => { + const { autoRefreshDaemon } = await import("../../open-sse/services/autoRefreshDaemon.ts"); + const originalFetch = globalThis.fetch; + const originalWarn = console.warn; + const warnings: string[] = []; + console.warn = (...args: unknown[]) => warnings.push(args.map(String).join(" ")); + // Simulate a provider whose validation request blows up (network error). + globalThis.fetch = (async () => { + throw new Error("ECONNRESET"); + }) as typeof fetch; + try { + autoRefreshDaemon.registerCredential("claude-web", "cookie-value"); + await autoRefreshDaemon.check(); + assert.ok( + warnings.some((w) => w.includes("claude-web") && w.includes("ECONNRESET")), + "expected a warn naming the provider and the network error, got: " + warnings.join(" | ") + ); + } finally { + globalThis.fetch = originalFetch; + console.warn = originalWarn; + autoRefreshDaemon.unregisterCredential("claude-web"); + } +}); + +// ── G10: batch item dispatch timeout ──────────────────────────────────────── +test("G10: hung batch item dispatch fails fast via wall-clock timeout", async () => { + const startedAt = Date.now(); + await assert.rejects( + withItemDispatchTimeout( + new Promise(() => {}), // never settles + 50, + "Batch item dispatch (/v1/chat/completions)" + ), + /timed out after 50ms/ + ); + const elapsed = Date.now() - startedAt; + assert.ok(elapsed < 5000, `timeout fired after ${elapsed}ms — too slow`); +}); + +test("G10: fast dispatch wins the race untouched", async () => { + const res = await withItemDispatchTimeout( + Promise.resolve(new Response("ok", { status: 200 })), + 1000, + "Batch item dispatch" + ); + assert.equal(res.status, 200); + assert.equal(await res.text(), "ok"); +}); From 74c9e7e0d2aff168b32c2bed0b1b7ebc13c5067b Mon Sep 17 00:00:00 2001 From: Jay Ongg <11032569+swingtempo@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:14:00 -0700 Subject: [PATCH 30/71] feat(combo): Auto-Combo snapshot generation/duplication in the UX (#10354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a snapshot-generation button to each Auto-Combo catalog card: computeSnapshotWeights() scores candidates (taskFit/stability/tierPriority/costInv) at combo-creation time instead of the previous hardcoded weight:1, and the new POST /api/combos/duplicate endpoint materializes any auto/* template into a persistent, editable static combo with normalized weights. Closes #10231. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 51 files): - 19/19 focused tests pass (snapshot-weights, combos-duplicate-route, combos-duplicate-resolution-audit) — covers auth gate (401/403), input validation (400/422), success shape, weight normalization, naming/dedup, and error-response sanitization (no stack traces). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: swingtempo --- open-sse/services/autoCombo/virtualFactory.ts | 59 +++- scripts/ad-hoc/dump-auto-combos.ts | 52 ++++ .../dashboard/combos/AutoComboCatalog.tsx | 70 ++++- src/app/(dashboard)/dashboard/combos/page.tsx | 11 +- src/app/api/combos/duplicate/route.ts | 181 +++++++++++ src/i18n/messages/ar.json | 7 +- src/i18n/messages/az.json | 7 +- src/i18n/messages/bg.json | 7 +- src/i18n/messages/bn.json | 7 +- src/i18n/messages/cs.json | 7 +- src/i18n/messages/da.json | 7 +- src/i18n/messages/de.json | 7 +- src/i18n/messages/en.json | 7 +- src/i18n/messages/es.json | 7 +- src/i18n/messages/fa.json | 7 +- src/i18n/messages/fi.json | 7 +- src/i18n/messages/fr.json | 7 +- src/i18n/messages/gu.json | 7 +- src/i18n/messages/he.json | 7 +- src/i18n/messages/hi.json | 7 +- src/i18n/messages/hu.json | 7 +- src/i18n/messages/id.json | 7 +- src/i18n/messages/in.json | 7 +- src/i18n/messages/it.json | 7 +- src/i18n/messages/ja.json | 7 +- src/i18n/messages/ko.json | 7 +- src/i18n/messages/mr.json | 7 +- src/i18n/messages/ms.json | 7 +- src/i18n/messages/nl.json | 7 +- src/i18n/messages/no.json | 7 +- src/i18n/messages/phi.json | 7 +- src/i18n/messages/pl.json | 7 +- src/i18n/messages/pt-BR.json | 7 +- src/i18n/messages/pt.json | 7 +- src/i18n/messages/ro.json | 7 +- src/i18n/messages/ru.json | 7 +- src/i18n/messages/sk.json | 7 +- src/i18n/messages/sv.json | 7 +- src/i18n/messages/sw.json | 7 +- src/i18n/messages/ta.json | 7 +- src/i18n/messages/te.json | 7 +- src/i18n/messages/th.json | 7 +- src/i18n/messages/tr.json | 7 +- src/i18n/messages/uk-UA.json | 7 +- src/i18n/messages/ur.json | 7 +- src/i18n/messages/vi.json | 7 +- src/i18n/messages/zh-CN.json | 7 +- src/i18n/messages/zh-TW.json | 7 +- src/shared/validation/schemas/combo.ts | 7 + .../combos-duplicate-resolution-audit.test.ts | 54 ++++ tests/unit/combos-duplicate-route.test.ts | 287 ++++++++++++++++++ tests/unit/snapshot-weights.test.ts | 243 +++++++++++++++ 52 files changed, 1215 insertions(+), 50 deletions(-) create mode 100644 scripts/ad-hoc/dump-auto-combos.ts create mode 100644 src/app/api/combos/duplicate/route.ts create mode 100644 tests/unit/combos-duplicate-resolution-audit.test.ts create mode 100644 tests/unit/combos-duplicate-route.test.ts create mode 100644 tests/unit/snapshot-weights.test.ts diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts index f468f23005..2eff8257ea 100644 --- a/open-sse/services/autoCombo/virtualFactory.ts +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -20,6 +20,7 @@ import { type AutoCategory, type AutoTier, } from "./suffixComposition"; +import { classifyTier } from "../tierResolver"; import type { AutoVariant } from "./autoPrefix"; import { buildFamilyCandidateFilter, type ModelFamily } from "./modelFamily"; import { getHiddenModelsByProvider } from "@/models"; @@ -602,6 +603,61 @@ export async function prepareVirtualAutoComboInputs( }; } +/** + * Score candidates at snapshot time using available data (capabilities, tier) + * and the mode-pack's dominant factors. Runtime telemetry (p95 latency, quota + * remaining) is not available during combo creation — this uses static signals only. + * + * Returns a map from modelStr → normalized weight score [0, 1]. + */ +export function computeSnapshotWeights( + candidates: readonly VirtualAutoComboCandidate[], + weights: ScoringWeights +): Map { + const scores = new Map(); + for (const c of candidates) { + let score = 0; + + // taskFit: reasoning + vision capable models score higher when taskFit is weighted + if (weights.taskFit > 0) { + if (c.resolvedReasoning || c.resolvedSupportsThinking) score += weights.taskFit * 0.6; + if (c.resolvedSupportsVision) score += weights.taskFit * 0.3; + } + + // stability: models with richer capabilities are assumed more stable + if (weights.stability > 0) { + const capabilityCount = + Number(c.resolvedReasoning ?? false) + + Number(c.resolvedSupportsThinking ?? false) + + Number(c.resolvedSupportsVision ?? false); + score += weights.stability * Math.min(capabilityCount / 2, 1); + } + + // Tier-based scoring (single classifyTier call covers both checks) + let tierInfo: { tier: string } | null = null; + if (weights.tierPriority > 0 || weights.costInv > 0) { + try { + tierInfo = classifyTier(c.provider, c.model); + } catch { + // fall through with zero + } + } + if (tierInfo && weights.tierPriority > 0 && tierInfo.tier === "premium") + score += weights.tierPriority; + if (tierInfo && weights.costInv > 0 && tierInfo.tier === "free") score += weights.costInv; + + // latencyInv: all candidates get a base score when latency matters + // (no runtime data at snapshot time, so equal baseline) + if (weights.latencyInv > 0) score += weights.latencyInv * 0.5; + + // health + quota: no runtime telemetry at snapshot time → neutral baseline + score += (weights.health + weights.quota) * 0.5; + + scores.set(c.modelStr, Math.min(score, 1)); + } + return scores; +} + function clonePreparedCandidates( candidates: readonly VirtualAutoComboCandidate[] ): VirtualAutoComboCandidate[] { @@ -770,6 +826,7 @@ export async function createVirtualAutoComboFromPrepared( } const providerPool = [...new Set(effectivePool.map((c) => c.provider))]; + const snapshotScores = computeSnapshotWeights(effectivePool, weights); const models = effectivePool.map((candidate, index) => ({ id: `virtual-auto-${variant || "default"}-${index + 1}-${candidate.provider}`, kind: "model" as const, @@ -779,7 +836,7 @@ export async function createVirtualAutoComboFromPrepared( ...(candidate.allowedConnectionIds ? { allowedConnectionIds: candidate.allowedConnectionIds } : {}), - weight: 1, + weight: snapshotScores.get(candidate.modelStr) ?? 1, label: candidate.provider, })); const autoConfig = { diff --git a/scripts/ad-hoc/dump-auto-combos.ts b/scripts/ad-hoc/dump-auto-combos.ts new file mode 100644 index 0000000000..0e3c352011 --- /dev/null +++ b/scripts/ad-hoc/dump-auto-combos.ts @@ -0,0 +1,52 @@ +/** + * One-shot diagnostic: resolve every built-in auto-combo template and dump the + * resulting candidate pool, weight pack, and config as JSON for inspection. + * + * Run from repo root: + * node --import tsx/esm scripts/ad-hoc/dump-auto-combos.ts > _tasks/research/auto-combos-snapshot.json + */ + +const { AUTO_TEMPLATE_VARIANTS, AUTO_SUFFIX_VARIANTS, AUTO_FAMILY_IDS } = + await import("@omniroute/open-sse/services/autoCombo/builtinCatalog"); +const { createBuiltinAutoCombo, prepareBuiltinAutoComboInputs } = + await import("@omniroute/open-sse/services/autoCombo/builtinCatalog"); + +// Prepares the candidate pool once (DB reads: connections, settings, capabilities) +const prepared = await prepareBuiltinAutoComboInputs(); + +const allTemplates: string[] = []; +allTemplates.push(...Object.keys(AUTO_TEMPLATE_VARIANTS)); +allTemplates.push(...AUTO_SUFFIX_VARIANTS); +allTemplates.push(...AUTO_FAMILY_IDS); + +const results: Array<{ + template: string; + candidateCount: number; + models: string[]; + weightPack: Record; + explorationRate: number; +}> = []; + +for (const name of allTemplates) { + try { + const suffix = name.slice("auto/".length); + const combo = await createBuiltinAutoCombo(name, suffix, prepared as never); + results.push({ + template: name, + candidateCount: combo.models.length, + models: combo.models.map((m) => m.model ?? `${m.providerId}/unknown`), + weightPack: combo.weights ?? {}, + explorationRate: combo.explorationRate, + }); + } catch (err) { + results.push({ + template: name, + candidateCount: 0, + models: [], + weightPack: {}, + explorationRate: 0, + }); + } +} + +console.log(JSON.stringify(results, null, 2)); diff --git a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx index a072fa8f1b..c3d6955148 100644 --- a/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx +++ b/src/app/(dashboard)/dashboard/combos/AutoComboCatalog.tsx @@ -1,19 +1,66 @@ "use client"; -import { useState } from "react"; +import { useState, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Card } from "@/shared/components"; -import { AUTO_COMBO_TEMPLATES } from "@/domain/assessment/types"; +import { AUTO_COMBO_TEMPLATES, type AutoComboTemplate } from "@/domain/assessment/types"; // Informational catalog of zero-config auto-routing combos. // Auto combos are resolved at request time by the chat handler based on the // currently connected providers / models — they have no row in the combos // table, so they were previously invisible in the UI. This panel surfaces // the static catalog (name, intent, categories, tiers, strategy) so users -// can discover the auto/ prefix without reading source. -export default function AutoComboCatalog() { +// can discover the auto/ prefix without reading source. Duplicate icon lets +// you materialize a snapshot into an editable static combo you can customize. +export default function AutoComboCatalog({ + onComboCreated, +}: { + onComboCreated?: (comboId: string) => void; +}) { const t = useTranslations("combos"); const [open, setOpen] = useState(false); + const [duplicatingName, setDuplicatingName] = useState(null); + + const handleDuplicateTemplate = useCallback( + async (template: AutoComboTemplate) => { + if ( + !confirm( + `${t("duplicateAutoComboConfirm", { name: template.name })}\n\n${t("duplicateAutoComboSnapshotMsg")}` + ) + ) + return; + + setDuplicatingName(template.name); + try { + const res = await fetch("/api/combos/duplicate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: template.name, strategy: template.strategy }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + alert( + `${t("duplicateAutoComboFailedPrefix")} ${data.error || t("duplicateAutoComboUnknownError")}` + ); + return; + } + + const combo = await res.json(); + + // Notify parent page to re-fetch combos so the new card renders. + onComboCreated?.(String(combo.id)); + } catch (err) { + console.error("Error duplicating auto-combo:", err); + alert( + `${t("duplicateAutoComboFailedPrefix")} ${err instanceof Error ? err.message : t("duplicateAutoComboUnknownError")}` + ); + } finally { + setDuplicatingName(null); + } + }, + [t, onComboCreated] + ); return ( @@ -44,8 +91,21 @@ export default function AutoComboCatalog() { {AUTO_COMBO_TEMPLATES.map((tpl) => (
+ +
{tpl.name} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index fd9d1e3b7e..fceada4eb5 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -879,6 +879,15 @@ export default function CombosPage() { } }; + const handleComboCreated = async (comboId: string) => { + await fetchData(); + // Wait for React to re-render the new card, then scroll it into view. + setTimeout(() => { + const el = document.querySelector(`[data-testid="combo-card-${comboId}"]`); + if (el) el.scrollIntoView({ behavior: "auto", block: "center" }); + }, 0); + }; + const handleDelete = async (id) => { if (!confirm(t("deleteConfirm"))) return; try { @@ -1104,7 +1113,7 @@ export default function CombosPage() {
- + ({ + id: `auto-duplicate-${name}-${index + 1}`, + kind: "model", + model: m.model || `${m.providerId}/unknown`, + weight: m.weight ?? 1, + }) + ); + + // Normalize models the same way /api/combos POST does (via normalizeComboModels). + const allCombos = await getCombos(); + const normalizedModels = normalizeComboModels(rawModels, { + comboName: `static-${name.replace("auto/", "")}`, + allCombos: allCombos as never, + }); + + if (normalizedModels.length === 0) { + return NextResponse.json( + { error: "No valid models resolved from this auto-combo template" }, + { status: 422 } + ); + } + + // Normalize scored weights so they sum to exactly 100. + const totalWeight = normalizedModels.reduce((s, m) => s + (m.weight ?? 0), 0); + if (totalWeight > 0 && normalizedModels.length > 0) { + for (const m of normalizedModels) { + m.weight = Math.max(1, Math.floor(((m.weight ?? 0) / totalWeight) * 100)); + } + let remainder = 100 - normalizedModels.reduce((s, m) => s + m.weight, 0); + for (let i = 0; i < normalizedModels.length && remainder > 0; i++) { + normalizedModels[i].weight++; + remainder--; + } + } + + // Generate a unique combo name based on the template (no "copy" appellation). + const baseName = `static-${name.replace("auto/", "")}`; + const existingNames = new Set(allCombos.map((c: any) => c.name)); + let newName = baseName; + let counter = 1; + while (existingNames.has(newName)) { + counter++; + newName = `${baseName} ${counter}`; + } + + // Capture the mode-pack weights from the virtual combo config so the snapshot + // preserves the scoring profile (quality-first, ship-fast, etc.) at creation time. + const weightPack = virtualCombo.weights ?? virtualCombo.autoConfig?.weights; + + // Create the static combo using the template's strategy. + const comboStrategy = strategy || "priority"; + const snapshotDate = new Date().toISOString(); + const comboData = await createCombo({ + name: newName, + models: normalizedModels, + strategy: comboStrategy, + description: `${name} @ ${snapshotDate}`, + config: { sourceAutoCombo: name, weightPack }, + version: 2, + }); + + return NextResponse.json(comboData, { status: 201 }); + } catch (error) { + console.error("Error duplicating auto-combo:", error); + return NextResponse.json( + { + error: "Failed to duplicate auto-combo", + details: + typeof error === "object" && error !== null && "message" in error + ? String(error.message) + : String(error), + }, + { status: 500 } + ); + } +} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 85bb06ddb8..c7eeeb3754 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -3722,7 +3722,12 @@ "errorDescription": "لم نتمكن من تحميل بيانات المجموعة في الوقت الحالي. تحقق من اتصالك وحاول مرة أخرى.", "errorId": "معرّف الخطأ: {id}", "errorRetry": "حاول مرة أخرى", - "comboLabel": "كومبو" + "comboLabel": "كومبو", + "duplicateAutoComboConfirm": "إنشاء مجموعة ثابتة من \"{name}\"؟", + "duplicateAutoComboSnapshotMsg": "سيؤدي هذا إلى التقاط المزودين/النماذج المتصلة حاليًا التي تطابق هذا القالب في مجموعة قابلة للتحرير.", + "duplicateAutoComboFailedPrefix": "فشل تكرار المجموعة التلقائية:", + "duplicateAutoComboUnknownError": "خطأ غير معروف", + "duplicateAutoComboTitle": "إنشاء مجموعة ثابتة من {name}" }, "costs": { "title": "التكاليف", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 6bf2e21e55..605fe9ed18 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "\"{name}\"-dan statik kombo yaratmaq?", + "duplicateAutoComboSnapshotMsg": "Bu, bu şablona uyğun olan hazırkı qoşulmuş provayderləri/modeləri redaktə oluna bilən kombo kimi saxlayacaq.", + "duplicateAutoComboFailedPrefix": "Avtokombo kopyalanması uğursuz oldu:", + "duplicateAutoComboUnknownError": "Naməlum xəta", + "duplicateAutoComboTitle": "{name}-dan statik kombo yarat" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index a8b6e486b3..b2dfd1176a 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -3722,7 +3722,12 @@ "errorDescription": "Не успяхме да заредим данните за комбинирането в момента. Проверете връзката си и опитайте отново.", "errorId": "Идентификатор на грешка: {id}", "errorRetry": "Опитай отново", - "comboLabel": "Комбо" + "comboLabel": "Комбо", + "duplicateAutoComboConfirm": "Да създадете статично комбо от \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Това ще направи моментна снимка на текущо свързаните доставчици/модели, които съвпадат с този шаблон, в редактируемо комбо.", + "duplicateAutoComboFailedPrefix": "Неуспешно копиране на автоматично комбо:", + "duplicateAutoComboUnknownError": "Неизвестна грешка", + "duplicateAutoComboTitle": "Създайте статично комбо от {name}" }, "costs": { "title": "Разходи", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index aadc026d06..34ecf02c74 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -3722,7 +3722,12 @@ "errorDescription": "আমরা এখন কম্বো ডেটা লোড করতে পারিনি। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।", "errorId": "ত্রুটি আইডি: {id}", "errorRetry": "পুনরায় চেষ্টা করুন", - "comboLabel": "কম্বো" + "comboLabel": "কম্বো", + "duplicateAutoComboConfirm": "\"{name}\" থেকে একটি স্ট্যাটিক কম্বো তৈরি করবেন?", + "duplicateAutoComboSnapshotMsg": "এটি এই টেমপ্লেটের সাথে মিলে যায় এমন বর্তমান সংযুক্ত প্রদানকারী/মডেলগুলিকে একটি সম্পাদনযোগ্য কম্বোতে স্ন্যাপশট নেবে।", + "duplicateAutoComboFailedPrefix": "অটোকম্বো ডুপ্লিকেশন ব্যর্থ:", + "duplicateAutoComboUnknownError": "অজানা ত্রুটি", + "duplicateAutoComboTitle": "{name} থেকে একটি স্ট্যাটিক কম্বো তৈরি করুন" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 5e95c8f357..18761a6f44 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Vytvořit statickou kombinaci z \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Tím se zachytí aktuálně připojení poskytovatelé/modely, které odpovídají této šabloně, do upravitelné kombinace.", + "duplicateAutoComboFailedPrefix": "Duplikace automatické kombinace selhala:", + "duplicateAutoComboUnknownError": "Neznámá chyba", + "duplicateAutoComboTitle": "Vytvořte statickou kombinaci z {name}" }, "costs": { "title": "Náklady", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index fdc9a5fc89..cd127ca8a3 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Opret en statisk kombination fra \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Dette vil tage et øjebliksbillede af de aktuelt tilsluttede udbydere/modeller, der matcher denne skabelon, i en redigerbar kombination.", + "duplicateAutoComboFailedPrefix": "Kopiering af auto-kombination mislykkedes:", + "duplicateAutoComboUnknownError": "Ukendt fejl", + "duplicateAutoComboTitle": "Opret en statisk kombination fra {name}" }, "costs": { "title": "Omkostninger", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 2b4c363146..d1f60bb142 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Eine statische Kombination aus \"{name}\" erstellen?", + "duplicateAutoComboSnapshotMsg": "Dadurch werden die aktuell verbundenen Anbieter/Modelle, die dieser Vorlage entsprechen, in einer bearbeitbaren Kombination gespeichert.", + "duplicateAutoComboFailedPrefix": "Automatische Kombination konnte nicht dupliziert werden:", + "duplicateAutoComboUnknownError": "Unbekannter Fehler", + "duplicateAutoComboTitle": "Erstelle eine statische Kombination aus {name}" }, "costs": { "title": "Kosten", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 92b0e725b4..2de1b5bd17 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3727,7 +3727,12 @@ "errorDescription": "We could not load combo data right now. Check your connection and try again.", "errorId": "Error ID: {id}", "errorRetry": "Try Again", - "comboLabel": "Combo" + "comboLabel": "Combo", + "duplicateAutoComboConfirm": "Create a static combo from \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "This will snapshot the currently connected providers/models that match this template into an editable combo.", + "duplicateAutoComboFailedPrefix": "Failed to duplicate auto-combo:", + "duplicateAutoComboUnknownError": "Unknown error", + "duplicateAutoComboTitle": "Create a static combo from {name}" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index a6b357e222..94b17341c1 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "¿Crear una combinación estática de \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Esto capturará los proveedores/modelos conectados actualmente que coincidan con esta plantilla en una combinación editable.", + "duplicateAutoComboFailedPrefix": "Error al duplicar la combinación automática:", + "duplicateAutoComboUnknownError": "Error desconocido", + "duplicateAutoComboTitle": "Crear una combinación estática de {name}" }, "costs": { "title": "Costos", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 35f12114ef..851f93aba9 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -3722,7 +3722,12 @@ "errorDescription": "در حال حاضر نمی‌توانیم داده‌های ترکیبی را بارگذاری کنیم. اتصال خود را بررسی کنید و دوباره تلاش کنید.", "errorId": "شناسه خطا: {id}", "errorRetry": "دوباره تلاش کنید", - "comboLabel": "ترکیب" + "comboLabel": "ترکیب", + "duplicateAutoComboConfirm": "ایجاد یک ترکیب ثابت از \"{name}\"؟", + "duplicateAutoComboSnapshotMsg": "این ارائه‌دهندگان/مدل‌های متصل فعلی که با این قالب مطابقت دارند را در یک ترکیب قابل ویرایش ذخیره می‌کند.", + "duplicateAutoComboFailedPrefix": "تکرار ترکیب خودکار ناموفق بود:", + "duplicateAutoComboUnknownError": "خطای ناشناخته", + "duplicateAutoComboTitle": "ایجاد یک ترکیب ثابت از {name}" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index fd15e9dfaa..9149e2ec4f 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -3722,7 +3722,12 @@ "errorDescription": "Emme voi ladata yhdistelmädataa juuri nyt. Tarkista yhteytesi ja yritä uudelleen.", "errorId": "Virhe ID: {id}", "errorRetry": "Yritä uudelleen", - "comboLabel": "Yhdistelmä" + "comboLabel": "Yhdistelmä", + "duplicateAutoComboConfirm": "Luodaanko staattinen yhdistelmä \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Tämä tallentaa tämän mallin mukaiset tällä hetkellä yhdistetyt tarjoajat/mallit muokattavaan yhdistelmään.", + "duplicateAutoComboFailedPrefix": "Automaattisen yhdistelmän kaksoiskappaleen luonti epäonnistui:", + "duplicateAutoComboUnknownError": "Tuntematon virhe", + "duplicateAutoComboTitle": "Luo staattinen yhdistelmä kohteesta {name}" }, "costs": { "title": "Kustannukset", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 64fc699674..72a3792df2 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Créer une combinaison statique à partir de \"{name}\" ?", + "duplicateAutoComboSnapshotMsg": "Cela capturera les fournisseurs/modèles actuellement connectés qui correspondent à ce modèle dans une combinaison modifiable.", + "duplicateAutoComboFailedPrefix": "Échec de la duplication de la combinaison automatique :", + "duplicateAutoComboUnknownError": "Erreur inconnue", + "duplicateAutoComboTitle": "Créer une combinaison statique à partir de {name}" }, "costs": { "title": "Coûts", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 5e21f097c1..a1a38f1980 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -3722,7 +3722,12 @@ "errorDescription": "અમે હાલમાં કોમ્બો ડેટા લોડ કરી શક્યા નથી. તમારી કનેક્શન તપાસો અને ફરી પ્રયાસ કરો.", "errorId": "ભૂલ આઈડી: {id}", "errorRetry": "ફરીથી પ્રયાસ કરો", - "comboLabel": "કોમ્બો" + "comboLabel": "કોમ્બો", + "duplicateAutoComboConfirm": "\"{name}\" માંથી સ્ટેટિક કોમ્બો બનાવશો?", + "duplicateAutoComboSnapshotMsg": "આ ટેમપ્લેટ સાથે મેચ થતા વર્તમાન જોડાયેલા પ્રદાતાઓ/મોડેલને સંપાદનીય કોમ્બોમાં સ્નેપશોટ લેશે.", + "duplicateAutoComboFailedPrefix": "ઓટોકોમ્બો ડુપ્લિકેટ કરવામાં નિષ્ફળ:", + "duplicateAutoComboUnknownError": "અજ્ઞાત ભૂલ", + "duplicateAutoComboTitle": "{name} માંથી સ્ટેટિક કોમ્બો બનાવો" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 90db68373a..96007496ae 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -3722,7 +3722,12 @@ "errorDescription": "לא הצלחנו לטעון את נתוני הקומבו כרגע. בדוק את החיבור שלך ונסה שוב.", "errorId": "שגיאת מזהה: {id}", "errorRetry": "נסה שוב", - "comboLabel": "קומבו" + "comboLabel": "קומבו", + "duplicateAutoComboConfirm": "ליצור קומבו סטטי מ־\"{name}\"?", + "duplicateAutoComboSnapshotMsg": "פעולה זו תצלם את הספקים/מודלים המחוברים כעת התואמים לתבנית הזו לקומבו ניתן לעריכה.", + "duplicateAutoComboFailedPrefix": "כשל בהעתיק קומבו אוטומטי:", + "duplicateAutoComboUnknownError": "שגיאה לא ידועה", + "duplicateAutoComboTitle": "צור קומבו סטטי מ־{name}" }, "costs": { "title": "עלויות", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 9e353a269b..3cbff8e202 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -3722,7 +3722,12 @@ "errorDescription": "हम अभी कॉम्बो डेटा लोड नहीं कर सके। कृपया अपनी कनेक्शन की जांच करें और फिर से प्रयास करें।", "errorId": "त्रुटि आईडी: {id}", "errorRetry": "फिर से प्रयास करें", - "comboLabel": "कॉम्बो" + "comboLabel": "कॉम्बो", + "duplicateAutoComboConfirm": "\"{name}\" से एक स्थैतिक कॉम्बो बनाएं?", + "duplicateAutoComboSnapshotMsg": "यह इस टेम्पलेट से मेल खाने वाले वर्तमान जुड़े प्रदाताओं/मॉडल को संपादन योग्य कॉम्बो में स्नैपशॉट लेगा।", + "duplicateAutoComboFailedPrefix": "ऑटोकॉम्बो डुप्लिकेट करने में विफल:", + "duplicateAutoComboUnknownError": "अज्ञात त्रुटि", + "duplicateAutoComboTitle": "{name} से एक स्थैतिक कॉम्बो बनाएं" }, "costs": { "title": "लागत", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 4f8ee2278b..6f723ed853 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -3722,7 +3722,12 @@ "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ó", + "duplicateAutoComboConfirm": "Létrehoz egy statikus kombinációt a(z) \"{name}\"-ból?", + "duplicateAutoComboSnapshotMsg": "Ez rögzíti az éppen csatlakoztatott szolgáltatókat/modelleket, amelyek megfelelnek ennek a sablonnak, egy szerkeszthető kombinációba.", + "duplicateAutoComboFailedPrefix": "Az automatikus kombináció duplikálása sikertelen:", + "duplicateAutoComboUnknownError": "Ismeretlen hiba", + "duplicateAutoComboTitle": "Hozzon létre statikus kombinációt a(z) {name}-ból" }, "costs": { "title": "Költségek", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index bc841a35d6..f6aa07123a 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Buat kombo statis dari \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Ini akan mengambil snapshot penyedia/model yang terhubung saat ini yang cocok dengan templat ini ke dalam kombo yang dapat diedit.", + "duplicateAutoComboFailedPrefix": "Gagal menduplikasi kombo otomatis:", + "duplicateAutoComboUnknownError": "Kesalahan tidak diketahui", + "duplicateAutoComboTitle": "Buat kombo statis dari {name}" }, "costs": { "title": "Biaya", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index b9c7e6706c..5f2587eb6a 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Buat kombo statis dari \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Ini akan mengambil snapshot penyedia/model yang terhubung saat ini yang cocok dengan templat ini ke dalam kombo yang dapat diedit.", + "duplicateAutoComboFailedPrefix": "Gagal menduplikasi kombo otomatis:", + "duplicateAutoComboUnknownError": "Kesalahan tidak diketahui", + "duplicateAutoComboTitle": "Buat kombo statis dari {name}" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index d777c72bf1..902ed0795a 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Creare una combinazione statica da \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Questo catturerà i fornitori/modelli attualmente connessi che corrispondono a questo modello in una combinazione modificabile.", + "duplicateAutoComboFailedPrefix": "Duplicazione della combinazione automatica fallita:", + "duplicateAutoComboUnknownError": "Errore sconosciuto", + "duplicateAutoComboTitle": "Crea una combinazione statica da {name}" }, "costs": { "title": "Costi", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index ee7b56d074..d586218bc8 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -3722,7 +3722,12 @@ "errorDescription": "現在、コンボデータを読み込むことができません。接続を確認して、再試行してください。", "errorId": "エラー ID: {id}", "errorRetry": "もう一度試してください", - "comboLabel": "コンボ" + "comboLabel": "コンボ", + "duplicateAutoComboConfirm": "\"{name}\"から静的コンボを作成しますか?", + "duplicateAutoComboSnapshotMsg": "このテンプレートに一致する現在接続されているプロバイダー/モデルを編集可能なコンボとしてスナップショットします。", + "duplicateAutoComboFailedPrefix": "オートコンボの複製に失敗しました:", + "duplicateAutoComboUnknownError": "不明なエラー", + "duplicateAutoComboTitle": "{name}から静的コンボを作成" }, "costs": { "title": "コスト", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 3a5feebf4e..4f10276f71 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -3722,7 +3722,12 @@ "errorDescription": "현재 콤보 데이터를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.", "errorId": "오류 ID: {id}", "errorRetry": "다시 시도해 주세요", - "comboLabel": "콤보" + "comboLabel": "콤보", + "duplicateAutoComboConfirm": "\"{name}\"에서 정적 콤보를 만드시겠습니까?", + "duplicateAutoComboSnapshotMsg": "이 템플릿과 일치하는 현재 연결된 제공자/모델을 편집 가능한 콤보로 스냅샷합니다.", + "duplicateAutoComboFailedPrefix": "자동 콤보 복제 실패:", + "duplicateAutoComboUnknownError": "알 수 없는 오류", + "duplicateAutoComboTitle": "{name}에서 정적 콤보 만들기" }, "costs": { "title": "비용", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 63afee6a7f..86437f4f3a 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -3722,7 +3722,12 @@ "errorDescription": "आम्ही सध्या कॉम्बो डेटा लोड करू शकत नाही. तुमचा कनेक्शन तपासा आणि पुन्हा प्रयत्न करा.", "errorId": "त्रुटी आयडी: {id}", "errorRetry": "पुन्हा प्रयत्न करा", - "comboLabel": "कॉम्बो" + "comboLabel": "कॉम्बो", + "duplicateAutoComboConfirm": "\"{name}\" मधून स्थिर कॉम्बो तयार करायचा?", + "duplicateAutoComboSnapshotMsg": "या टेम्पलेटशी जुळणारे सध्या कनेक्ट केलेले प्रदाता/मॉडेल्स संपादनयोग्य कॉम्बोमध्ये स्नॅपशॉट घेईल.", + "duplicateAutoComboFailedPrefix": "ऑटोकॉम्बो डुप्लिकेट करण्यात अपयश:", + "duplicateAutoComboUnknownError": "अज्ञात त्रुटी", + "duplicateAutoComboTitle": "{name} मधून स्थिर कॉम्बो तयार करा" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index b6c322c3e2..498af0d138 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Cipta kombo statik dari \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Ini akan mengambil snapshot penyedia/model yang disambungkan semasa yang sepadan dengan templat ini ke dalam kombo yang boleh diedit.", + "duplicateAutoComboFailedPrefix": "Gagal menduplikasi kombo automatik:", + "duplicateAutoComboUnknownError": "Ralat tidak diketahui", + "duplicateAutoComboTitle": "Cipta kombo statik dari {name}" }, "costs": { "title": "Kos", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 397c206888..6422dad14d 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Een statische combinatie maken van \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Dit maakt een momentopname van de momenteel verbonden providers/modellen die overeenkomen met dit sjabloon in een bewerkbare combinatie.", + "duplicateAutoComboFailedPrefix": "Automatische combinatie dupliceren mislukt:", + "duplicateAutoComboUnknownError": "Onbekende fout", + "duplicateAutoComboTitle": "Maak een statische combinatie van {name}" }, "costs": { "title": "Kosten", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 502bbae12e..6f105c14c3 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Opprett en statisk kombinasjon fra \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Dette vil ta et øyeblikksbilde av de nåvørende tilkoblede leverandørene/modellene som matcher denne malen i en redigerbar kombinasjon.", + "duplicateAutoComboFailedPrefix": "Duplisering av auto-kombinasjon mislyktes:", + "duplicateAutoComboUnknownError": "Ukjent feil", + "duplicateAutoComboTitle": "Opprett en statisk kombinasjon fra {name}" }, "costs": { "title": "Kostnader", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 5285b3d18c..f9dc8a149b 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Gumawa ng static combo mula sa \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Ito ay kukuha ng snapshot ng kasalukuyang nakakonektang mga provider/model na tugma sa template na ito sa isang editable na combo.", + "duplicateAutoComboFailedPrefix": "Nabigo ang pag-duplicate ng auto-combo:", + "duplicateAutoComboUnknownError": "Hindi alam na error", + "duplicateAutoComboTitle": "Gumawa ng static combo mula sa {name}" }, "costs": { "title": "Mga gastos", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index e29e73670e..2bca5453b1 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Utworzyć statyczną kombinację z \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Spowoduje to przechwycenie aktualnie połączonych dostawców/modeli pasujących do tego szablonu w edytowalnej kombinacji.", + "duplicateAutoComboFailedPrefix": "Nie udało się skopiować automatycznej kombinacji:", + "duplicateAutoComboUnknownError": "Nieznany błąd", + "duplicateAutoComboTitle": "Utwórz statyczną kombinację z {name}" }, "costs": { "title": "Koszty", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 4ba29762d0..9a3b93def6 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -3727,7 +3727,12 @@ "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", + "duplicateAutoComboConfirm": "Criar uma combinação estática de \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Isso irá capturar os provedores/modelos atualmente conectados que correspondem a este modelo em uma combinação editável.", + "duplicateAutoComboFailedPrefix": "Falha ao duplicar a combinação automática:", + "duplicateAutoComboUnknownError": "Erro desconhecido", + "duplicateAutoComboTitle": "Criar uma combinação estática de {name}" }, "costs": { "title": "Custos", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 05441018a3..dc1a90c803 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Criar uma combinação estática de \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Isto irá capturar os provedores/modelos atualmente conectados que correspondem a este modelo em uma combinação editável.", + "duplicateAutoComboFailedPrefix": "Falha ao duplicar a combinação automática:", + "duplicateAutoComboUnknownError": "Erro desconhecido", + "duplicateAutoComboTitle": "Criar uma combinação estática de {name}" }, "costs": { "title": "Custos", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index b8ef63cbb8..4265215f2e 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Creați o combinație statică din \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Acesta va captura furnizorii/modelurile conectate în prezent care se potrivesc cu acest șablon într-o combinație editabilă.", + "duplicateAutoComboFailedPrefix": "Duplicarea combinației automate a eșuat:", + "duplicateAutoComboUnknownError": "Eroare necunoscută", + "duplicateAutoComboTitle": "Creați o combinație statică din {name}" }, "costs": { "title": "Costuri", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index cf9ff5e8c2..8210e133a2 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -3722,7 +3722,12 @@ "errorDescription": "Мы не смогли загрузить данные комбо в данный момент. Проверьте ваше соединение и попробуйте снова.", "errorId": "Идентификатор ошибки: {id}", "errorRetry": "Попробуйте снова", - "comboLabel": "Комбо" + "comboLabel": "Комбо", + "duplicateAutoComboConfirm": "Создать статическое комбо из \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Это создаст снимок текущих подключенных провайдеров/моделей, соответствующих этому шаблону, в редактируемом комбо.", + "duplicateAutoComboFailedPrefix": "Не удалось дублировать автоматическое комбо:", + "duplicateAutoComboUnknownError": "Неизвестная ошибка", + "duplicateAutoComboTitle": "Создать статическое комбо из {name}" }, "costs": { "title": "Затраты", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 10bf077a43..ba4a09a022 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Vytvoriť staticú kombináciu z \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Tým sa zachytí aktuálne pripojení poskytovatelia/modely, ktoré zodpovedajú tejto šablóne, do upraviteľnej kombinácie.", + "duplicateAutoComboFailedPrefix": "Duplikácia automatickej kombinácie zlyhala:", + "duplicateAutoComboUnknownError": "Neznáma chyba", + "duplicateAutoComboTitle": "Vytvorte staticú kombináciu z {name}" }, "costs": { "title": "náklady", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 46f53972d7..1433b5b49c 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Skapa en statisk kombination från \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Detta kommer att ta en ögonblicksbild av de för närvarande anslutna leverantörerna/modellerna som matchar denna mall i en redigerbar kombination.", + "duplicateAutoComboFailedPrefix": "Kopiering av automatisk kombination misslyckades:", + "duplicateAutoComboUnknownError": "Okänt fel", + "duplicateAutoComboTitle": "Skapa en statisk kombination från {name}" }, "costs": { "title": "Kostnader", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 12d19b28c1..3a2b3c204c 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "Tengeneza combo thabiti kutoka \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Hii itachukua picha ya watoa huduma/watolei sambazwa sasa yanayolingana na kioo hiki katika combo inayoedit.", + "duplicateAutoComboFailedPrefix": "Imeshindwa kuiga combo ya otomatiki:", + "duplicateAutoComboUnknownError": "Hitilafai isiyojulikana", + "duplicateAutoComboTitle": "Tengeneza combo thabiti kutoka {name}" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 2e80cf7f22..ab9fc3c1f3 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -3722,7 +3722,12 @@ "errorDescription": "நாங்கள் தற்போது கம்போ தரவுகளை ஏற்ற முடியவில்லை. உங்கள் இணைப்பை சரிபார்க்கவும் மற்றும் மீண்டும் முயற்சிக்கவும்.", "errorId": "பிழை அடையாளம்: {id}", "errorRetry": "மீண்டும் முயற்சி செய்", - "comboLabel": "கொம்போ" + "comboLabel": "கொம்போ", + "duplicateAutoComboConfirm": "\"{name}\" இலிருந்து நிலையான கம்போ உருவாக்கவா?", + "duplicateAutoComboSnapshotMsg": "இந்த டெம்ப்ளேட்டுடன் பொருந்தும் தற்போதைய இணைக்கப்பட்ட வழங்குநர்கள்/மாதிரிகளை திருத்தக்கூடிய கம்போவில் எடுக்கும்.", + "duplicateAutoComboFailedPrefix": "ஆட்டோகம்போ நகலெடுத்தல் தோல்வியடைந்தது:", + "duplicateAutoComboUnknownError": "அறியப்படாத பிழை", + "duplicateAutoComboTitle": "{name} இலிருந்து நிலையான கம்போ உருவாக்கவும்" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 73032e0d13..6d1a07e41b 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -3722,7 +3722,12 @@ "errorDescription": "మేము ప్రస్తుతం కాంబో డేటాను లోడ్ చేయలేకపోయాము. మీ కనెక్షన్‌ను తనిఖీ చేసి మళ్లీ ప్రయత్నించండి.", "errorId": "లోపం ID: {id}", "errorRetry": "మరలా ప్రయత్నించండి", - "comboLabel": "కాంబో" + "comboLabel": "కాంబో", + "duplicateAutoComboConfirm": "\"{name}\" నుండి స్థిర కంబో సృష్టించాలా?", + "duplicateAutoComboSnapshotMsg": "ఈ టెంప్లేట్‌తో సరిపోయే ప్రస్తుత కనెక్ట్ చేసిన ప్రొవైడర్లు/మోడల్‌లను ఎడిటబుల్ కంబోలో స్నాప్‌షాట్ తీసుకుంటుంది.", + "duplicateAutoComboFailedPrefix": "ఆటోకంబో డూప్లికేట్ చేయడంలో విఫలమైంది:", + "duplicateAutoComboUnknownError": "తెలియని దోషం", + "duplicateAutoComboTitle": "{name} నుండి స్థిర కంబో సృష్టించండి" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 4b004659a9..b30a5bebc5 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -3722,7 +3722,12 @@ "errorDescription": "ไม่สามารถโหลดข้อมูลคอมโบได้ในขณะนี้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองอีกครั้ง.", "errorId": "รหัสข้อผิดพลาด: {id}", "errorRetry": "ลองอีกครั้ง", - "comboLabel": "คอมโบ" + "comboLabel": "คอมโบ", + "duplicateAutoComboConfirm": "สร้างคอมโบคงที่จาก \"{name}\" หรือไม่?", + "duplicateAutoComboSnapshotMsg": "สิ่งนี้จะจับภาพผู้ให้บริการ/โมเดลที่เชื่อมต่ออยู่ในปัจจุบันซึ่งตรงกับเทมเพลตนี้ลงในคอมโบที่แก้ไขได้", + "duplicateAutoComboFailedPrefix": "ล้มเหลวในการทำสำเนาคอมโบอัตโนมัติ:", + "duplicateAutoComboUnknownError": "ข้อผิดพลาดที่ไม่ทราบสาเหตุ", + "duplicateAutoComboTitle": "สร้างคอมโบคงที่จาก {name}" }, "costs": { "title": "ค่าใช้จ่าย", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index abd8bfc297..958b931040 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -3722,7 +3722,12 @@ "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", + "duplicateAutoComboConfirm": "\"{name}\"-den statik bir kombinasyon oluşturulsun mu?", + "duplicateAutoComboSnapshotMsg": "Bu, bu şablona uygun olarak şu anda bağlı olan sağlayıcıları/modelleri düzenlenebilir bir kombinasyonda yakalayacaktır.", + "duplicateAutoComboFailedPrefix": "Otomatik kombinasyon kopyalanamadı:", + "duplicateAutoComboUnknownError": "Bilinmeyen hata", + "duplicateAutoComboTitle": "{name}-den statik bir kombinasyon oluştur" }, "costs": { "title": "Maliyetler", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index ce9ede8d17..586571a9ca 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -3722,7 +3722,12 @@ "errorDescription": "Ми не змогли завантажити дані комбо прямо зараз. Перевірте своє з'єднання та спробуйте ще раз.", "errorId": "Ідентифікатор помилки: {id}", "errorRetry": "Спробуйте ще раз", - "comboLabel": "Комбо" + "comboLabel": "Комбо", + "duplicateAutoComboConfirm": "Створити статичне комбо з \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Це зробить знімок поточно підключених постачальників/моделей, які відповідають цьому шаблону, у редаговане комбо.", + "duplicateAutoComboFailedPrefix": "Не вдалося дублювати автоматичне комбо:", + "duplicateAutoComboUnknownError": "Невідома помилка", + "duplicateAutoComboTitle": "Створити статичне комбо з {name}" }, "costs": { "title": "Витрати", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index b5c237a7d5..aef5f7ae73 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -3722,7 +3722,12 @@ "errorDescription": "ہم اس وقت کومبو ڈیٹا لوڈ نہیں کر سکے۔ اپنی کنکشن چیک کریں اور دوبارہ کوشش کریں۔", "errorId": "خرابی کی شناخت: {id}", "errorRetry": "پھر کوشش کریں", - "comboLabel": "کمبو" + "comboLabel": "کمبو", + "duplicateAutoComboConfirm": "\"{name}\" سے ایک جامد کمبو بنائیں؟", + "duplicateAutoComboSnapshotMsg": "یہ اس ٹیمپلیٹ سے ملنے والے موجودہ منسلک فراہم کنندگان/ماڈلز کو ایڈیٹ ایبل کمبو میں اسنیپ شاট لے گا۔", + "duplicateAutoComboFailedPrefix": "آٹو کمبو ڈپلیکیٹ کرنے میں ناکام:", + "duplicateAutoComboUnknownError": "نامعلوم خرابی", + "duplicateAutoComboTitle": "{name} سے ایک جامد کمبو بنائیں" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 9cf64c0d2f..0004b404b9 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -3727,7 +3727,12 @@ "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", + "duplicateAutoComboConfirm": "Tạo một tổ hợp tĩnh từ \"{name}\"?", + "duplicateAutoComboSnapshotMsg": "Điều này sẽ chụp lại các nhà cung cấp/mô hình đang kết nối hiện tại phù hợp với mẫu này vào một tổ hợp có thể chỉnh sửa.", + "duplicateAutoComboFailedPrefix": "Không thể sao chép tổ hợp tự động:", + "duplicateAutoComboUnknownError": "Lỗi không xác định", + "duplicateAutoComboTitle": "Tạo tổ hợp tĩnh từ {name}" }, "costs": { "title": "Chi phí", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index b4b6bad59b..05eed0cd99 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -3722,7 +3722,12 @@ "errorDescription": "我们现在无法加载组合数据。请检查您的连接并重试。", "errorId": "错误 ID: {id}", "errorRetry": "再试一次", - "comboLabel": "组合" + "comboLabel": "组合", + "duplicateAutoComboConfirm": "从\"{name}\"创建静态组合?", + "duplicateAutoComboSnapshotMsg": "这将把与此模板匹配的当前连接提供者/模型快照到可编辑的组合中。", + "duplicateAutoComboFailedPrefix": "复制自动组合失败:", + "duplicateAutoComboUnknownError": "未知错误", + "duplicateAutoComboTitle": "从{name}创建静态组合" }, "costs": { "title": "成本", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index b2d9679086..442b9a66e7 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -3722,7 +3722,12 @@ "errorDescription": "目前無法加載組合數據。請檢查您的連接並重試。", "errorId": "錯誤 ID: {id}", "errorRetry": "再試一次", - "comboLabel": "組合" + "comboLabel": "組合", + "duplicateAutoComboConfirm": "從\"{name}\"建立靜態組合?", + "duplicateAutoComboSnapshotMsg": "這將把與此模板匹配的目前連線提供者/模型快照到可編輯的組合中。", + "duplicateAutoComboFailedPrefix": "複製自動組合失敗:", + "duplicateAutoComboUnknownError": "未知錯誤", + "duplicateAutoComboTitle": "從{name}建立靜態組合" }, "costs": { "title": "成本", diff --git a/src/shared/validation/schemas/combo.ts b/src/shared/validation/schemas/combo.ts index c616e63372..edeca47e72 100644 --- a/src/shared/validation/schemas/combo.ts +++ b/src/shared/validation/schemas/combo.ts @@ -444,3 +444,10 @@ export const reorderCombosSchema = z export const testComboSchema = z.object({ comboName: z.string().trim().min(1, "comboName is required"), }); + +// POST /api/combos/duplicate - Resolve an auto-combo template (e.g. "auto/best-coding") +// into a static, editable combo snapshot. +export const duplicateAutoComboSchema = z.object({ + name: z.string().trim().min(1, 'Missing required field: "name" (e.g. auto/best-coding)'), + strategy: comboStrategySchema.optional(), +}); diff --git a/tests/unit/combos-duplicate-resolution-audit.test.ts b/tests/unit/combos-duplicate-resolution-audit.test.ts new file mode 100644 index 0000000000..ea304e8a85 --- /dev/null +++ b/tests/unit/combos-duplicate-resolution-audit.test.ts @@ -0,0 +1,54 @@ +/** + * Audit: every built-in auto-combo template must resolve to SOME spec or variant + * (not an empty object that produces a full unfiltered pool). + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-duplicate-audit-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "dup-audit-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const { AUTO_TEMPLATE_VARIANTS, AUTO_SUFFIX_VARIANTS, AUTO_FAMILY_IDS } = + await import("@omniroute/open-sse/services/autoCombo/builtinCatalog"); +const { resolveBuiltinAutoSpec } = + await import("@omniroute/open-sse/services/autoCombo/builtinCatalog"); + +test.after(() => { + core.resetDbInstance(); + try { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } catch {} +}); + +const ALL_TEMPLATES = [ + ...Object.keys(AUTO_TEMPLATE_VARIANTS), + ...AUTO_SUFFIX_VARIANTS, + ...AUTO_FAMILY_IDS, +]; + +test("every template resolves to a non-empty spec or variant (not bare unfiltered pool)", async () => { + for (const name of ALL_TEMPLATES) { + const suffix = name.slice("auto/".length); + const r = resolveBuiltinAutoSpec(name, suffix); + + // auto/chat-style templates legitimately return {variant: undefined} — that IS the "unconstrained" spec. + if (Object.prototype.hasOwnProperty.call(AUTO_TEMPLATE_VARIANTS, name)) { + continue; + } + // Family IDs handled by duplicate route fallback + if (AUTO_FAMILY_IDS.includes(name)) { + continue; + } + + assert.ok( + JSON.stringify(r) !== "{}", + `${name} resolved to empty spec — would produce full unfiltered pool` + ); + } +}); diff --git a/tests/unit/combos-duplicate-route.test.ts b/tests/unit/combos-duplicate-route.test.ts new file mode 100644 index 0000000000..447c26583b --- /dev/null +++ b/tests/unit/combos-duplicate-route.test.ts @@ -0,0 +1,287 @@ +/** + * Unit tests for POST /api/combos/duplicate (Rule #18). + * + * Coverage: auth gate, input validation (400), success response shape (201) + * including weight distribution and naming convention, config fields, + * and error sanitization (no stack traces in responses). + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "os"; +import path from "node:path"; + +// ── DB / auth setup ─────────────────────────────────────────────────────────── + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combos-duplicate-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "combos-duplicate-test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); + +// Route loaded AFTER env is set +const duplicateRoute = await import("../../src/app/api/combos/duplicate/route.ts"); + +function makePostRequest(url: string, body: unknown, apiKey?: string): Request { + return new Request(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}), + }, + body: JSON.stringify(body), + }); +} + +test.after(() => { + core.resetDbInstance(); + try { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } catch { + // best-effort cleanup + } +}); + +// ── Auth gate ───────────────────────────────────────────────────────────────── + +test("POST /api/combos/duplicate returns 401/403 when auth is required and no token", async () => { + await settingsDb.updateSettings({ requireLogin: true }); + process.env.INITIAL_PASSWORD = "test-password-dup"; + + const req = makePostRequest("http://localhost/api/combos/duplicate", { + name: "auto/best-coding", + }); + const res = await duplicateRoute.POST(req as never); + + assert.ok( + res.status === 401 || res.status === 403, + `Expected 401 or 403 without auth, got ${res.status}` + ); + + await settingsDb.updateSettings({ requireLogin: false }); + delete process.env.INITIAL_PASSWORD; +}); + +test("POST /api/combos/duplicate passes auth with valid management API key", async () => { + await settingsDb.updateSettings({ requireLogin: true }); + process.env.INITIAL_PASSWORD = "test-password-dup-key"; + const { key } = await apiKeysDb.createApiKey("dup-test", "machine-dup", ["manage"]); + + // Invalid body (empty name) — auth should pass, business logic rejects + const req = makePostRequest("http://localhost/api/combos/duplicate", { name: "" }, key); + const res = await duplicateRoute.POST(req as never); + + assert.ok( + !(res.status === 401 || res.status === 403), + `Auth should have passed with valid key, got ${res.status}` + ); + assert.equal(res.status, 400); + + await settingsDb.updateSettings({ requireLogin: false }); + delete process.env.INITIAL_PASSWORD; +}); + +// ── Input validation ──────────────────────────────────────────────────────── + +test("POST /api/combos/duplicate returns 400 when name is missing", async () => { + await settingsDb.updateSettings({ requireLogin: false }); + + const req = makePostRequest("http://localhost/api/combos/duplicate", {}); + const res = await duplicateRoute.POST(req as never); + const body = await res.json(); + + assert.equal(res.status, 400); + assert.ok( + typeof body.error === "string" && body.error.length > 0, + "should return an error message" + ); +}); + +test("POST /api/combos/duplicate returns 400 when name is not a string", async () => { + await settingsDb.updateSettings({ requireLogin: false }); + + const req = makePostRequest("http://localhost/api/combos/duplicate", { + name: 123, + }); + const res = await duplicateRoute.POST(req as never); + + assert.equal(res.status, 400); +}); + +test("POST /api/combos/duplicate returns 400 when name is empty string", async () => { + await settingsDb.updateSettings({ requireLogin: false }); + + const req = makePostRequest("http://localhost/api/combos/duplicate", { + name: "", + }); + const res = await duplicateRoute.POST(req as never); + + assert.equal(res.status, 400); +}); + +// ── Success response shape (when models match) ─────────────────────────────── + +test("POST /api/combos/duplicate returns valid combo with correct naming and weights on success", async () => { + await settingsDb.updateSettings({ requireLogin: false }); + + const req = makePostRequest("http://localhost/api/combos/duplicate", { + name: "auto/best-coding", + }); + const res = await duplicateRoute.POST(req as never); + const body = await res.json(); + + if (res.status === 201) { + // --- Naming convention: starts with static- and ends with "copy" (or "copy N") --- + assert.ok( + typeof body.name === "string" && body.name.length > 0, + "response should contain combo name" + ); + assert.ok( + body.name.startsWith("static-"), + `Combo name should start with 'static-', got: ${body.name}` + ); + assert.ok( + /^static-[\w-]+(\s+\d+)?$/.test(body.name), + `Combo name should be 'static-