diff --git a/.env.example b/.env.example
index c809f7653c05..20c43643a211 100644
--- a/.env.example
+++ b/.env.example
@@ -229,6 +229,15 @@ PORT=20128
# unaffected by this dev-only flag).
OMNIROUTE_USE_TURBOPACK=1
+# Disable systemd sd_notify (Type=notify / WatchdogSec=) even when running
+# under a systemd unit with NOTIFY_SOCKET set.
+# Used by: scripts/dev/systemd-notify.mjs. Set to 1 to disable.
+# OMNIROUTE_DISABLE_SD_NOTIFY=1
+
+# Injected by systemd when running under a service unit (sd_notify protocol).
+# Read by scripts/dev/systemd-notify.mjs — never set this yourself.
+# NOTIFY_SOCKET=/run/systemd/notify
+
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip.
# OMNIROUTE_SKIP_DB_HEALTHCHECK=1
@@ -408,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).
@@ -696,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.
@@ -876,13 +899,21 @@ 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
# MODEL_SYNC_INTERVAL_HOURS=24
# Provider limits sync interval in minutes (rate limit windows, quotas).
-# Used by: src/server-init.ts — polls provider health endpoints.
+# Used by: src/lib/usage/providerLimits.ts — polls provider health endpoints.
# Default: 70
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
@@ -1033,6 +1064,10 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs().
#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000
+# WAL truncate cadence override (ms). Set to 0 to disable. Default: 21600000 (6h).
+# Used by: src/lib/db/core.ts::getWalTruncateIntervalMs().
+#OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS=21600000
+
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0
@@ -1348,6 +1383,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).
@@ -1402,6 +1445,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
@@ -1975,6 +2026,16 @@ APP_LOG_TO_FILE=true
# Reachability probe target for the scheduler and the auto-test endpoint.
# Point it at an internal/self-hosted URL to avoid the public default.
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
+# Probes started at once per batch, for the scheduler and the auto-test endpoint.
+# Floored at 1 and capped at 50. Default: 10.
+# PROXY_HEALTH_TEST_CONCURRENCY=10
+# Delay in ms between two probe departures inside a batch. Without it the whole batch
+# leaves at once and a shared egress IP can trip a rate-limited target. 0 disables the
+# spacing; capped at 5000. Default: 100.
+# PROXY_HEALTH_TEST_STAGGER_MS=100
+# Set "false" to stop probing the real host of a proxy's assigned provider (GET /models,
+# no API key) and always use the generic target above instead. Default: enabled.
+# PROXY_HEALTH_USE_PROVIDER_TARGET=true
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.
@@ -2117,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/.github/dependabot.yml b/.github/dependabot.yml
index 8db8504007dc..3dfad903a7ae 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/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 36f82549779e..d12585363a61 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
+ - uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
languages: javascript-typescript
queries: security-extended
- - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
+ - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:javascript-typescript"
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 6e4adc019290..d8a65576dc67 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -372,7 +372,7 @@ jobs:
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v4.37.6
+ uses: github/codeql-action/upload-sarif@v4.37.7
with:
sarif_file: trivy-results.sarif
category: trivy-image
diff --git a/.github/workflows/radar-export.yml b/.github/workflows/radar-export.yml
new file mode 100644
index 000000000000..043de88d3df0
--- /dev/null
+++ b/.github/workflows/radar-export.yml
@@ -0,0 +1,64 @@
+# Publica o export estável do catálogo consumido pelo OmniRoute Radar numa URL
+# fixa (asset de release `radar-export-latest`), para o servidor privado do Radar
+# (1 GB RAM, nunca clona/builda o OmniRoute) baixá-lo via `RADAR_EXPORT_URL` em
+# vez de depender do snapshot gravado no deploy. Fonte: scripts/release/radar-export.mjs.
+#
+# A URL estável resultante (definir em RADAR_EXPORT_URL no .env do radar-server):
+# https://github.com/diegosouzapw/OmniRoute/releases/download/radar-export-latest/export-omniroute.json
+name: Radar Export
+
+on:
+ workflow_dispatch: # o operador pode publicar sob demanda (de qualquer ref)
+ push:
+ branches: [main] # produção: só o catálogo do main clobra o asset estável
+ paths:
+ - open-sse/config/freeModelCatalog.data.ts
+ - open-sse/config/freeModelCatalog.ts
+ - open-sse/config/providerRegistry.ts
+ - open-sse/config/providers/**
+ - scripts/release/radar-export.mjs
+ - .github/workflows/radar-export.yml
+ schedule:
+ - cron: "17 6 * * 1" # semanal (segunda 06:17 UTC): mantém geradoEm/proveniência frescos
+
+permissions:
+ contents: read
+
+concurrency:
+ group: radar-export-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ CI_NODE_VERSION: "24"
+
+jobs:
+ publish-export:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # gh release upload — clobra o asset estável do export
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false # publish usa GH_TOKEN via gh release, não a credencial do checkout
+ - uses: actions/setup-node@v7
+ with:
+ node-version: ${{ env.CI_NODE_VERSION }}
+ cache: npm
+ - run: npm ci
+ - name: Generate catalog export with provenance
+ run: node --import tsx/esm scripts/release/radar-export.mjs "$RUNNER_TEMP/export-omniroute.json"
+ - name: Publish to the stable release asset
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+ TAG="radar-export-latest"
+ # Cria o release estável na primeira vez; nas seguintes só re-anexa o asset.
+ if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
+ gh release create "$TAG" \
+ --repo "$GITHUB_REPOSITORY" \
+ --title "Radar catalog export (rolling)" \
+ --notes "Export estável do catálogo OmniRoute para o Radar. Atualizado automaticamente; NÃO é um release de versão do produto." \
+ --latest=false
+ fi
+ gh release upload "$TAG" "$RUNNER_TEMP/export-omniroute.json" --repo "$GITHUB_REPOSITORY" --clobber
diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts
index bc18518caddf..be985361c952 100644
--- a/@omniroute/opencode-plugin/src/index.ts
+++ b/@omniroute/opencode-plugin/src/index.ts
@@ -76,6 +76,17 @@ import {
type FreeModelFreeType,
} from "./naming.js";
+/**
+ * Minimal leveled logger sink accepted by the default fetchers and the static
+ * catalog builder. A full `Logger` satisfies it structurally; the config hook
+ * injects the same partial shape (see `createOmniRouteConfigHook` deps).
+ */
+type OmniRouteLoggerSink = {
+ error?: (message: string, ...args: unknown[]) => void;
+ warn: (message: string, ...args: unknown[]) => void;
+ debug?: (message: string, ...args: unknown[]) => void;
+};
+
/**
* Zod schema for plugin options accepted as the second element of the
* `plugin: [name, opts]` tuple in opencode.json. Strict by design — unknown
@@ -791,13 +802,18 @@ export async function forceSyncOmniRouteModels(args: {
try {
rawCombos = await combosFetcher(auth.baseURL, auth.managementReadToken, 10_000);
} catch (err) {
- console.warn("[omniroute-plugin] force sync: combos fetch failed", err);
+ logger.warn("force sync: combos fetch failed", err);
}
}
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
if (wantAutoCombos) {
try {
- rawAutoCombos = await autoCombosFetcher(auth.baseURL, auth.managementReadToken, 5_000);
+ rawAutoCombos = await autoCombosFetcher(
+ auth.baseURL,
+ auth.managementReadToken,
+ 5_000,
+ logger
+ );
} catch {
/* soft-fail */
}
@@ -1089,7 +1105,7 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
return {
auth: createOmniRouteAuthHook(resolved),
- provider: createOmniRouteProviderHook(resolved, { cache: sharedCache }),
+ provider: createOmniRouteProviderHook(resolved, { cache: sharedCache, logger }),
config: configWithSyncCommand,
tool: {
omniroute_sync_models: syncTool,
@@ -1294,10 +1310,15 @@ export function mapRawModelToModelV2(
// `(providerID, modelID)`. If the raw id is already provider-prefixed
// (e.g. `cc/claude-opus-4-7` from the `cc` Claude Code alias, or
// `nvidia/llama-3-70b` from a provider that ships prefixed ids), leave
- // it as-is — double-prefixing breaks OC's lookup. Otherwise prefix with
- // the resolved `providerId` so a bare key like `claude-opus-4` parses as
- // `(omniroute, claude-opus-4)` and the credentials resolve correctly.
- id: raw.id.includes("/") ? raw.id : `${ctx.providerId}/${raw.id}`,
+ // it as-is — double-prefixing breaks OC's lookup. Bare **combo** ids
+ // (`owned_by: "combo"`, e.g. `gpt-5.6-sol`) must also stay unprefixed:
+ // OpenCode looks up `-m /` as model id `` under
+ // the plugin provider (#10345). Other bare ids still prefix with
+ // `providerId` so credentials resolve as `(omniroute, model)`.
+ id:
+ raw.id.includes("/") || raw.owned_by === "combo"
+ ? raw.id
+ : `${ctx.providerId}/${raw.id}`,
/**
* Display name. Falls back to raw.id when no enrichment is available;
* the caller (`createOmniRouteProviderHook`) overlays
@@ -1671,7 +1692,8 @@ export interface OmniRouteRawAutoCombo {
export type OmniRouteAutoCombosFetcher = (
baseURL: string,
apiKey: string,
- timeoutMs?: number
+ timeoutMs?: number,
+ logger?: OmniRouteLoggerSink
) => Promise;
/**
@@ -1683,9 +1705,11 @@ export type OmniRouteAutoCombosFetcher = (
export const defaultOmniRouteAutoCombosFetcher: OmniRouteAutoCombosFetcher = async (
baseURL,
apiKey,
- timeoutMs = 5_000
+ timeoutMs = 5_000,
+ logger?: OmniRouteLoggerSink
) => {
if (!apiKey || !baseURL) return [];
+ const log = logger ?? _logger;
const trimmed = trimTrailingSlashes(baseURL);
const root = trimmed.replace(/\/v\d+$/, "");
@@ -1704,15 +1728,11 @@ export const defaultOmniRouteAutoCombosFetcher: OmniRouteAutoCombosFetcher = asy
});
// 404 = endpoint not deployed yet — expected during rollout
if (res.status === 404) {
- console.warn(
- `[omniroute-plugin] /api/combos/auto not available (404) — auto combos disabled`
- );
+ log.warn(`/api/combos/auto not available (404) — auto combos disabled`);
return [];
}
if (!res.ok) {
- console.warn(
- `[omniroute-plugin] /api/combos/auto failed: ${res.status} ${res.statusText} — auto combos disabled`
- );
+ log.warn(`/api/combos/auto failed: ${res.status} ${res.statusText} — auto combos disabled`);
return [];
}
const body = (await res.json()) as unknown;
@@ -1730,8 +1750,8 @@ export const defaultOmniRouteAutoCombosFetcher: OmniRouteAutoCombosFetcher = asy
return out;
} catch (err) {
// Network error, timeout, abort — all non-fatal
- console.warn(
- `[omniroute-plugin] /api/combos/auto fetch failed: ${err instanceof Error ? err.message : String(err)} — auto combos disabled`
+ log.warn(
+ `/api/combos/auto fetch failed: ${err instanceof Error ? err.message : String(err)} — auto combos disabled`
);
return [];
} finally {
@@ -2930,10 +2950,7 @@ export function passesModelAllowlist(
* filter is set, all combos pass. Combos with zero resolvable members pass
* (mirrors `isUsableCombo` semantics).
*/
-export function passesComboAllowlist(
- combo: OmniRouteRawCombo,
- visible?: ModelListFilter
-): boolean {
+export function passesComboAllowlist(combo: OmniRouteRawCombo, visible?: ModelListFilter): boolean {
if (!visible) return true;
const steps = Array.isArray(combo.models) ? combo.models : [];
if (steps.length === 0) return true;
@@ -3125,9 +3142,15 @@ export function createOmniRouteProviderHook(
providersFetcher?: OmniRouteProvidersFetcher;
now?: () => number;
cache?: OmniRouteFetchCache;
+ logger?: _Logger;
} = {}
): ProviderHook {
const resolved = resolveOmniRoutePluginOptions(opts);
+ const logger =
+ deps.logger ??
+ createLogger(
+ resolved.features?.startupDebug ? "debug" : (resolved.features?.logLevel ?? "warn")
+ );
const fetcher = deps.fetcher ?? defaultOmniRouteModelsFetcher;
// T-05: combo discovery merges `/api/combos` entries into the same map as
// `/v1/models`. Default fetcher is declared further down the file; the
@@ -3201,8 +3224,8 @@ export function createOmniRouteProviderHook(
: undefined) ??
"";
if (!baseURL) {
- console.warn(
- `[omniroute-plugin] provider.models(${resolved.providerId}): ` +
+ logger.error(
+ `provider.models(${resolved.providerId}): ` +
`no baseURL resolvable — checked plugin opts, auth.json, and provider config. ` +
`Set baseURL in opencode.json plugin options or run \`opencode connect ${resolved.providerId}\` with a baseURL.`
);
@@ -3233,8 +3256,8 @@ export function createOmniRouteProviderHook(
rawModels = await fetcher(baseURL, apiKey, 10_000);
// T-05: combos fetch is best-effort, gated by features.combos.
- // Soft-fail on any error: emit a console.warn and fall back to a
- // models-only catalog. Rationale: /api/combos requires a
+ // Soft-fail on any error: emit a warn-level diagnostic and fall back
+ // to a models-only catalog. Rationale: /api/combos requires a
// management-scoped key and OmniRoute may not have any combos
// provisioned. Hard-failing when combos are optional would
// silently hide the whole provider from OC's picker.
@@ -3243,10 +3266,7 @@ export function createOmniRouteProviderHook(
try {
rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
- console.warn(
- "[omniroute-plugin] combos fetch failed, falling back to models-only catalog",
- err
- );
+ logger.warn("combos fetch failed, falling back to models-only catalog", err);
}
}
@@ -3256,7 +3276,7 @@ export function createOmniRouteProviderHook(
rawAutoCombos = [];
if (wantAutoCombos) {
try {
- rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
+ rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000, logger);
} catch {
// Already handled inside the default fetcher — this catch
// is belt-and-suspenders for injected stubs.
@@ -3270,10 +3290,7 @@ export function createOmniRouteProviderHook(
try {
rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
- console.warn(
- "[omniroute-plugin] enrichment fetch failed, falling back to raw ids",
- err
- );
+ logger.warn("enrichment fetch failed, falling back to raw ids", err);
}
}
@@ -3288,7 +3305,7 @@ export function createOmniRouteProviderHook(
10_000
);
} catch (err) {
- console.warn("[omniroute-plugin] compression-metadata fetch failed", err);
+ logger.warn("compression-metadata fetch failed", err);
}
}
@@ -3302,8 +3319,8 @@ export function createOmniRouteProviderHook(
try {
rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
} catch (err) {
- console.warn(
- "[omniroute-plugin] /api/providers fetch failed; usableOnly filter disabled for this refresh",
+ logger.warn(
+ "/api/providers fetch failed; usableOnly filter disabled for this refresh",
err
);
}
@@ -3322,8 +3339,9 @@ export function createOmniRouteProviderHook(
// Debug breadcrumb: surface fetch result so operators can confirm
// the dynamic pipeline fired and how much catalog OmniRoute returned.
// Emitted once per cache miss (TTL refresh) — quiet on cache hits.
- console.warn(
- `[omniroute-plugin] catalog refreshed for providerId=${resolved.providerId} baseURL=${baseURL}: ` +
+ // Info-level: hidden at the default `warn` level (see #8982).
+ logger.info(
+ `catalog refreshed for providerId=${resolved.providerId} baseURL=${baseURL}: ` +
`${rawModels.length} models + ${rawCombos.length} combos + ` +
`${rawEnrichment.size} enrichment entries + ` +
`${rawCompressionCombos.length} compression combos + ` +
@@ -3603,9 +3621,7 @@ export function createOmniRouteProviderHook(
const dedupeKey = `${cacheKey}::${comboKey}`;
if (!collisionWarned.has(dedupeKey)) {
collisionWarned.add(dedupeKey);
- console.warn(
- `[omniroute-plugin] combo key "${comboKey}" collides with a model id; combo wins.`
- );
+ logger.warn(`combo key "${comboKey}" collides with a model id; combo wins.`);
}
}
}
@@ -3623,8 +3639,8 @@ export function createOmniRouteProviderHook(
}
if (pending.length > 0) {
- console.warn(
- `[omniroute-plugin] ${pending.length} combo(s) could not resolve all nested combo-refs after ${MAX_COMBO_PASSES} passes; they will advertise context=0 to avoid over-claiming.`
+ logger.warn(
+ `${pending.length} combo(s) could not resolve all nested combo-refs after ${MAX_COMBO_PASSES} passes; they will advertise context=0 to avoid over-claiming.`
);
}
@@ -4268,8 +4284,10 @@ export function buildStaticProviderEntry(
enrichment?: OmniRouteEnrichmentMap,
compressionCombos?: OmniRouteCompressionCombo[],
connections?: OmniRouteProviderConnection[],
- rawAutoCombos?: OmniRouteRawAutoCombo[]
+ rawAutoCombos?: OmniRouteRawAutoCombo[],
+ logger?: OmniRouteLoggerSink
): OmniRouteStaticProviderEntry {
+ const log = logger ?? _logger;
const models: Record = {};
const rawModelKeys = new Set();
@@ -4647,8 +4665,8 @@ export function buildStaticProviderEntry(
}
if (pendingStatic.length > 0) {
- console.warn(
- `[omniroute-plugin] ${pendingStatic.length} combo(s) in the static catalog could not resolve all nested combo-refs after ${MAX_STATIC_COMBO_PASSES} passes; they will be omitted.`
+ log.warn(
+ `${pendingStatic.length} combo(s) in the static catalog could not resolve all nested combo-refs after ${MAX_STATIC_COMBO_PASSES} passes; they will be omitted.`
);
}
@@ -4669,9 +4687,7 @@ export function buildStaticProviderEntry(
const isExpectedRawTwin = autoCombo.id === key && rawModelKeys.has(key);
if (!isExpectedRawTwin && !reportedCollisions.has(key)) {
reportedCollisions.add(key);
- console.warn(
- `[omniroute-plugin] auto combo key "${key}" collides with an existing model; auto combo wins.`
- );
+ log.warn(`auto combo key "${key}" collides with an existing model; auto combo wins.`);
}
}
models[key] = entry;
@@ -5342,7 +5358,8 @@ export function createOmniRouteConfigHook(
warmSnapshot = snapshotResult;
// Log snapshot age (accept any age — instant beats empty).
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
- const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
+ const ageLabel =
+ typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
logAt(
"warn",
`config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
@@ -5394,7 +5411,12 @@ export function createOmniRouteConfigHook(
const doAutoCombos = async (): Promise => {
if (!wantAutoCombos) return;
try {
- localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
+ localRawAutoCombos = await autoCombosFetcher(
+ baseURL,
+ managementReadToken,
+ 5_000,
+ logger
+ );
} catch {
// Already handled inside the default fetcher
}
@@ -5415,7 +5437,11 @@ export function createOmniRouteConfigHook(
const doCompression = async (): Promise => {
if (!wantCompressionMeta) return;
try {
- localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
+ localRawCompressionCombos = await compressionMetaFetcher(
+ baseURL,
+ managementReadToken,
+ 10_000
+ );
} catch (err) {
logAt(
"error",
@@ -5528,7 +5554,8 @@ export function createOmniRouteConfigHook(
localRawEnrichment,
localRawCompressionCombos,
localRawConnections,
- localRawAutoCombos
+ localRawAutoCombos,
+ logger
);
const inputWithProvider2 = input as { provider?: Record };
if (inputWithProvider2.provider) {
@@ -5618,7 +5645,8 @@ export function createOmniRouteConfigHook(
rawEnrichment,
rawCompressionCombos,
rawConnections,
- rawAutoCombos
+ rawAutoCombos,
+ logger
);
// Mutate the input.provider map. The Config type declares
diff --git a/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts b/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts
new file mode 100644
index 000000000000..f7afda9ab63d
--- /dev/null
+++ b/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts
@@ -0,0 +1,34 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { mapRawModelToModelV2 } from "../src/index.ts";
+
+test("mapRawModelToModelV2: bare combo ids stay unprefixed (#10345)", () => {
+ const combo = mapRawModelToModelV2(
+ {
+ id: "gpt-5.6-sol",
+ owned_by: "combo",
+ context_length: 272000,
+ max_output_tokens: 8192,
+ },
+ { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
+ );
+ assert.equal(combo.id, "gpt-5.6-sol");
+ assert.equal(combo.providerID, "omniroute");
+
+ const slashed = mapRawModelToModelV2(
+ {
+ id: "cx/gpt-5.6-sol",
+ owned_by: "combo",
+ context_length: 272000,
+ },
+ { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
+ );
+ assert.equal(slashed.id, "cx/gpt-5.6-sol");
+
+ const ordinary = mapRawModelToModelV2(
+ { id: "claude-primary", context_length: 200000 },
+ { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
+ );
+ assert.equal(ordinary.id, "omniroute/claude-primary");
+});
diff --git a/@omniroute/opencode-plugin/tests/log-level.test.ts b/@omniroute/opencode-plugin/tests/log-level.test.ts
index 7e73cb4eb827..58566aba6d07 100644
--- a/@omniroute/opencode-plugin/tests/log-level.test.ts
+++ b/@omniroute/opencode-plugin/tests/log-level.test.ts
@@ -5,8 +5,14 @@ import { join } from "node:path";
import test from "node:test";
import type { Config } from "@opencode-ai/plugin";
-import { createOmniRouteConfigHook, OmniRoutePlugin } from "../src/index.js";
-import { getLogLevel, logger, setLogLevel, type LogLevel } from "../src/logger.js";
+import {
+ createOmniRouteConfigHook,
+ createOmniRouteProviderHook,
+ defaultOmniRouteAutoCombosFetcher,
+ OmniRoutePlugin,
+ type OmniRouteRawModelEntry,
+} from "../src/index.js";
+import { createLogger, getLogLevel, logger, setLogLevel, type LogLevel } from "../src/logger.js";
type ConsoleMethod = "error" | "info" | "log" | "warn";
type ConsoleEntries = Record;
@@ -216,3 +222,105 @@ test("logger error output remains visible at error level", async () => {
setLogLevel(previousLevel);
}
});
+
+const MINIMAL_MODELS: OmniRouteRawModelEntry[] = [
+ {
+ id: "claude-primary",
+ object: "model",
+ owned_by: "combo",
+ capabilities: { tool_calling: true, reasoning: true, vision: true, thinking: true },
+ context_length: 200000,
+ max_output_tokens: 64000,
+ input_modalities: ["text", "image"],
+ output_modalities: ["text"],
+ },
+];
+
+function providerHookWithLevel(level: LogLevel, baseURL?: string) {
+ return createOmniRouteProviderHook(
+ {
+ baseURL,
+ features: { autoCombos: false, enrichment: false, logLevel: level },
+ },
+ {
+ fetcher: async () => MINIMAL_MODELS,
+ combosFetcher: async () => {
+ throw new Error("combos boom");
+ },
+ }
+ );
+}
+
+test("logLevel error suppresses provider.models() fallback warnings and the catalog-refresh breadcrumb", async () => {
+ const hook = providerHookWithLevel("error", "https://or.example.com/v1");
+ const lines = rendered(
+ await captureConsole(async () => {
+ await hook.models!({} as never, { auth: { type: "api", key: "sk-x" } as never });
+ })
+ );
+
+ assert.equal(lines.filter((line) => line.includes("combos fetch failed")).length, 0);
+ assert.equal(lines.filter((line) => line.includes("catalog refreshed")).length, 0);
+});
+
+test("logLevel debug preserves the provider.models() catalog-refresh breadcrumb", async () => {
+ const hook = providerHookWithLevel("debug", "https://or.example.com/v1");
+ const lines = rendered(
+ await captureConsole(async () => {
+ await hook.models!({} as never, { auth: { type: "api", key: "sk-x" } as never });
+ })
+ );
+
+ assert.ok(
+ lines.some((line) => line.includes("catalog refreshed")),
+ "catalog-refresh breadcrumb emitted at debug level"
+ );
+});
+
+test("no baseURL resolvable stays visible at error level", async () => {
+ const hook = providerHookWithLevel("error");
+ const lines = rendered(
+ await captureConsole(async () => {
+ await hook.models!({} as never, { auth: { type: "api", key: "sk-x" } as never });
+ })
+ );
+
+ assert.ok(
+ lines.some((line) => line.includes("no baseURL resolvable")),
+ "genuine misconfiguration error remains visible at error level"
+ );
+});
+
+test("default auto-combos fetcher 404 warning respects the threaded logger level", async () => {
+ const originalFetch = globalThis.fetch;
+ (globalThis as { fetch: unknown }).fetch = (async () => ({
+ status: 404,
+ ok: false,
+ })) as typeof fetch;
+ try {
+ const silent = await captureConsole(async () => {
+ await defaultOmniRouteAutoCombosFetcher(
+ "https://or.example.com/v1",
+ "sk-x",
+ 5_000,
+ createLogger("error")
+ );
+ });
+ assert.equal(rendered(silent).length, 0, "404 warning suppressed at error level");
+
+ const loud = await captureConsole(async () => {
+ await defaultOmniRouteAutoCombosFetcher(
+ "https://or.example.com/v1",
+ "sk-x",
+ 5_000,
+ createLogger("warn")
+ );
+ });
+ assert.ok(
+ rendered(loud).some((line) => line.includes("/api/combos/auto not available")),
+ "404 warning emitted at warn level"
+ );
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
diff --git a/AGENTS.md b/AGENTS.md
index b1bcfebafdd7..d4a7e7eb8051 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, 341 LLM providers, auto-fallback.
+**OmniRoute** — unified AI proxy/router. One endpoint, 346 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -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/CHANGELOG.md b/CHANGELOG.md
index 94438a092287..9c01b39a7a09 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/Dockerfile b/Dockerfile
index de9b5a14997b..8eca2c3bd202 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/README.md b/README.md
index d04278cab301..390acbe4724a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
-
+
@@ -63,7 +63,7 @@
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
| ------------------------- | :-----: | :---------: | :---------: |
-| 🌐 Providers | 290 | **341** | more queued |
+| 🌐 Providers | 290 | **342** | more queued |
| 🧠 Documented models | 1185 | **1202** | — |
| 🖼️ Modality Bridge | — | 🆕 vision | video |
| 📡 Radar free catalog | — | 🆕 opt-in | — |
@@ -101,7 +101,7 @@
⚙️ Features
🎯 Combos
- 🌐 Providers
+ 🌐 Providers
🔌 CLI & MCP
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
-
+
@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step:
-
+
📊 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 **341-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)
-## 🌐 341 AI Providers — 90+ Free
+## 🌐 346 AI Providers — 90+ Free
-> The most complete catalog of any open-source router: **341 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**.
@@ -988,6 +988,8 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
```
+`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
+
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
> branch. These mutable tags are intended only for testing unreleased fixes and
@@ -1172,7 +1174,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c
Runtime Node.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27
Language TypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0)
Framework Next.js 16 + React 19 + Tailwind CSS 4
-
Database better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 154 migrations
+
Database better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 157 migrations
Memory SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay
Schemas Zod 4 — MCP tool I/O validation + API contracts
Protocols MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)
@@ -1495,7 +1497,7 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
Project ⭐ How it inspired OmniRoute
TOON 24.9k Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage.
- GCF – Graph Compact Format 22 First inspired our tabular compaction stage; now its zero-dependency, lossless generic-profile encoder is vendored directly as the Headroom codec (MIT, SPDX-marked), current with GCF spec v3.2.
+ GCF – Graph Compact Format 22 First inspired our tabular compaction stage; now its zero-dependency, lossless generic-profile encoder is vendored directly as the Headroom codec (MIT, SPDX-marked), with later numeric-domain and count-mismatch correctness fixes.
token-optimizer-mcp 444 Brotli/SQLite cache + per-session context-delta — inspired our session-dedup engine.
token-savior 1.1k Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction.
token-saver 117 Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip.
diff --git a/bin/cli/api.mjs b/bin/cli/api.mjs
index fff6cf0829c4..6534f9109558 100644
--- a/bin/cli/api.mjs
+++ b/bin/cli/api.mjs
@@ -52,6 +52,19 @@ function resolveUrl(path, opts) {
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
}
+/** The machine-derived token is valid only for the local loopback server. */
+export function isLoopbackUrl(value) {
+ try {
+ const hostname = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase();
+ if (hostname === "localhost" || hostname === "::1") return true;
+ if (/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)) return true;
+ if (/^::ffff:(?:127\.|7f[0-9a-f]{2}:)/i.test(hostname)) return true;
+ return false;
+ } catch {
+ return false;
+ }
+}
+
export async function buildHeaders(opts) {
const headers = new Headers(opts.headers || {});
if (!headers.has("accept")) headers.set("accept", "application/json");
@@ -87,10 +100,17 @@ export async function buildHeaders(opts) {
if (auth && !headers.has("authorization")) {
headers.set("authorization", `Bearer ${auth}`);
}
- // Inject machine-id derived CLI token; env var override for testing.
- const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
- if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
- headers.set(CLI_TOKEN_HEADER, cliToken);
+ // Inject the machine-derived credential only for an explicit local loopback
+ // destination. Remote contexts and absolute remote URLs use scoped access
+ // tokens and must never receive this machine-bound local credential.
+ const destinationUrl = opts.destinationUrl ?? getBaseUrl(opts);
+ if (!isLoopbackUrl(destinationUrl)) {
+ headers.delete(CLI_TOKEN_HEADER);
+ } else {
+ const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
+ if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
+ headers.set(CLI_TOKEN_HEADER, cliToken);
+ }
}
if (opts.idempotencyKey && !headers.has("idempotency-key")) {
headers.set("idempotency-key", opts.idempotencyKey);
@@ -195,8 +215,12 @@ function fetchOnce(url, init, timeoutMs) {
export async function apiFetch(path, opts = {}) {
const method = String(opts.method || "GET").toUpperCase();
const url = resolveUrl(path, opts);
- const headers = await buildHeaders(opts);
+ const headers = await buildHeaders({ ...opts, destinationUrl: url });
const body = serializeBody(opts.body, headers);
+ // Undici preserves custom headers across cross-origin redirects. A local server
+ // redirect must never turn the loopback machine credential into an outbound
+ // secret, so fail redirects whenever this header is present.
+ const redirect = headers.has(CLI_TOKEN_HEADER) ? "error" : opts.redirect;
const timeout =
opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000);
const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts);
@@ -205,7 +229,7 @@ export async function apiFetch(path, opts = {}) {
let lastErr;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
- const res = await fetchOnce(url, { method, headers, body }, timeout);
+ const res = await fetchOnce(url, { method, headers, body, redirect }, timeout);
if (res.ok) return enrichResponse(res, opts);
if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) {
const delay = computeBackoff(attempt, res.headers.get("retry-after"));
diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs
index 9ac34bf63667..817013dd195d 100644
--- a/bin/cli/commands/doctor.mjs
+++ b/bin/cli/commands/doctor.mjs
@@ -4,7 +4,9 @@ import os from "node:os";
import path from "node:path";
import { createDecipheriv, scryptSync } from "node:crypto";
import { fileURLToPath, pathToFileURL } from "node:url";
+import { isLoopbackUrl } from "../api.mjs";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
+import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
@@ -378,11 +380,11 @@ function checkMemory() {
});
}
-async function fetchWithTimeout(url) {
+async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
try {
- return await fetch(url, { signal: controller.signal });
+ return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeout);
}
@@ -471,6 +473,98 @@ async function checkServerLiveness(options = {}) {
);
}
+export async function checkMachineTokenAuth(options = {}) {
+ if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") {
+ return warn("CLI machine token", "CLI machine-token authentication is disabled", {
+ derived: false,
+ accepted: false,
+ disabled: true,
+ tokenExposed: false,
+ });
+ }
+
+ let url;
+ try {
+ const parsed = new URL(resolveLivenessUrl(options));
+ if (
+ !["http:", "https:"].includes(parsed.protocol) ||
+ parsed.username ||
+ parsed.password ||
+ !isLoopbackUrl(parsed.toString())
+ ) {
+ return warn(
+ "CLI machine token",
+ "Machine-token probes are limited to HTTP(S) loopback endpoints",
+ { derived: false, accepted: false, tokenExposed: false }
+ );
+ }
+ parsed.pathname = "/api/cli/whoami";
+ parsed.search = "";
+ parsed.hash = "";
+ url = parsed.toString();
+ } catch {
+ return warn("CLI machine token", "Could not resolve the management endpoint", {
+ derived: false,
+ accepted: false,
+ tokenExposed: false,
+ });
+ }
+
+ const token = await getCliToken();
+ if (!token) {
+ return fail(
+ "CLI machine token",
+ "Could not derive a machine token; verify the node-machine-id runtime is installed",
+ { derived: false, accepted: false, tokenExposed: false }
+ );
+ }
+
+ try {
+ const response = await fetchWithTimeout(url, {
+ headers: { [CLI_TOKEN_HEADER]: token },
+ redirect: "error",
+ });
+ if (response.ok) {
+ return ok("CLI machine token", "Server accepted the local machine token", {
+ url,
+ status: response.status,
+ derived: true,
+ accepted: true,
+ tokenExposed: false,
+ });
+ }
+ if (response.status === 401 || response.status === 403) {
+ return warn(
+ "CLI machine token",
+ "Server rejected the local machine token; if the CLI and server are on different hosts or container boundaries, run `omniroute connect --key `",
+ {
+ url,
+ status: response.status,
+ derived: true,
+ accepted: false,
+ containerBoundaryLikely: true,
+ tokenExposed: false,
+ }
+ );
+ }
+ return warn("CLI machine token", `Machine-token probe returned HTTP ${response.status}`, {
+ url,
+ status: response.status,
+ derived: true,
+ accepted: false,
+ tokenExposed: false,
+ });
+ } catch {
+ return warn("CLI machine token", "Machine-token endpoint could not be reached", {
+ url,
+ status: 0,
+ derived: true,
+ accepted: false,
+ tokenExposed: false,
+ });
+ }
+}
+
export async function collectDoctorChecks(context = {}, options = {}) {
const rootDir =
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
@@ -488,6 +582,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
if (!options.skipLiveness) {
checks.push(await checkServerLiveness(options));
+ checks.push(await checkMachineTokenAuth(options));
}
// CLI tool health checks
diff --git a/bin/cli/commands/providers.mjs b/bin/cli/commands/providers.mjs
index 91d60cead884..eb872241bf41 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 8802f75cd1c8..ec10c246496a 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/tray/autostart.mjs b/bin/cli/tray/autostart.mjs
index 6c1ba21aee53..a61a5c739aab 100644
--- a/bin/cli/tray/autostart.mjs
+++ b/bin/cli/tray/autostart.mjs
@@ -121,7 +121,16 @@ function writeLinuxSystemdUnit(cliPath) {
"Wants=network-online.target",
"",
"[Service]",
- "Type=simple",
+ // Type=notify + WatchdogSec: the server sends READY=1 once listening and
+ // WATCHDOG=1 every 60s; if its event loop ever blocks (frozen process),
+ // the pings stop and systemd kills+restarts the service. NotifyAccess=all
+ // because the pings come from the server child, not the serve supervisor.
+ // Foreground serve only: `--daemon` escapes the cgroup and would break
+ // the notify handshake.
+ "Type=notify",
+ "NotifyAccess=all",
+ "WatchdogSec=180",
+ "TimeoutStartSec=300",
`ExecStart=${buildServeExecLine(cliPath, { tray: false })}`,
"Restart=on-failure",
"RestartSec=5",
diff --git a/bin/cli/tui/ProvidersTestAll.jsx b/bin/cli/tui/ProvidersTestAll.jsx
index 73fc78614a27..c1911888ac0c 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/bin/cli/utils/cliToken.mjs b/bin/cli/utils/cliToken.mjs
index 94691ba95277..38895c13bf0e 100644
--- a/bin/cli/utils/cliToken.mjs
+++ b/bin/cli/utils/cliToken.mjs
@@ -12,25 +12,39 @@ function getActiveSalt() {
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
}
-export async function getCliToken() {
- const salt = getActiveSalt();
- if (_cached !== null && _cachedSalt === salt) return _cached;
+export function deriveCliToken(machineIdModule, salt) {
try {
// node-machine-id is CommonJS: under `await import()` its exports land on
// `.default`, so destructuring `machineIdSync` off the namespace yields
// undefined and calling it throws — which the catch below turned into an
// empty token, silently disabling CLI auth for every management request.
// Same resolution order as src/lib/machineToken.ts.
- const mod = await import("node-machine-id");
- const machineIdSync = mod.machineIdSync ?? mod.default?.machineIdSync;
- if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable");
+ const machineIdSync =
+ machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
+ if (typeof machineIdSync !== "function") return "";
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
- const mid = machineIdSync(true);
- _cached = crypto.createHmac("sha256", mid).update(salt).digest("hex");
+ const rawId = machineIdSync(true);
+ if (!rawId) return "";
+ return crypto.createHmac("sha256", rawId).update(salt).digest("hex");
+ } catch {
+ return "";
+ }
+}
+
+export async function getCliToken() {
+ const salt = getActiveSalt();
+ if (_cached !== null && _cachedSalt === salt) return _cached;
+ try {
+ const imported = await import("node-machine-id");
+ const token = deriveCliToken(imported, salt);
+ if (!token) {
+ // Swallowing here changes control flow (every management call goes out
+ // unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
+ console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled");
+ }
+ _cached = token;
} catch (e) {
- // Swallowing here changes control flow (every management call goes out
- // unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled:", e);
_cached = "";
}
diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs
index c023879da823..fb0a4555208e 100755
--- a/bin/omniroute.mjs
+++ b/bin/omniroute.mjs
@@ -119,6 +119,9 @@ function loadEnvFile() {
addEnvPath(join(ROOT, ".env"));
}
+ const keyOrigin = new Map();
+ const shadowed = new Map();
+
for (const envPath of envPaths) {
try {
if (existsSync(envPath)) {
@@ -131,19 +134,31 @@ function loadEnvFile() {
const key = trimmed.slice(0, eqIdx).trim();
if (process.env[key] === undefined) {
process.env[key] = parseEnvValue(trimmed.slice(eqIdx + 1));
+ keyOrigin.set(key, envPath);
+ } else if (!shadowed.has(key)) {
+ // The line is inert: something set this key first. Report it once
+ // per key, whether the winner was an earlier file or the process
+ // environment (#6194: a shell's own HOSTNAME beat the .env and the
+ // server bound to the wrong address in silence).
+ shadowed.set(key, { winner: keyOrigin.get(key) ?? null, loser: envPath });
}
}
}
loadedEnvPaths.push(envPath);
}
- } catch {
- // Ignore errors reading env files.
+ } catch (err) {
+ console.warn(` \x1b[33m⚠ Could not read ${envPath}: ${err?.message ?? err}\x1b[0m`);
}
}
for (const envPath of loadedEnvPaths) {
console.log(` \x1b[2m📋 Loaded env from ${envPath}\x1b[0m`);
}
+
+ for (const [key, { winner, loser }] of shadowed) {
+ const setter = winner ? winner : "the environment";
+ console.warn(` \x1b[33m⚠ ${key} in ${loser} is ignored, ${setter} set it first\x1b[0m`);
+ }
}
loadEnvFile();
diff --git a/changelog.d/features/10303-healthz-event-loop-lag.md b/changelog.d/features/10303-healthz-event-loop-lag.md
new file mode 100644
index 000000000000..991c12302183
--- /dev/null
+++ b/changelog.d/features/10303-healthz-event-loop-lag.md
@@ -0,0 +1 @@
+- **feat(resilience):** warn when `/healthz` is served under event-loop lag ≥200ms so a slow 200 is visible as sick, not healthy ([#10303](https://github.com/diegosouzapw/OmniRoute/issues/10303))
diff --git a/changelog.d/features/10316-livez-endpoint.md b/changelog.d/features/10316-livez-endpoint.md
new file mode 100644
index 000000000000..01409d7b48b0
--- /dev/null
+++ b/changelog.d/features/10316-livez-endpoint.md
@@ -0,0 +1 @@
+- **feat(docker):** add `GET`/`HEAD` `/livez` as a process-alive probe, distinct from `/healthz` readiness ([#10316](https://github.com/diegosouzapw/OmniRoute/issues/10316))
diff --git a/changelog.d/features/10587-ogg-speech-alias.md b/changelog.d/features/10587-ogg-speech-alias.md
new file mode 100644
index 000000000000..118e2a7b4839
--- /dev/null
+++ b/changelog.d/features/10587-ogg-speech-alias.md
@@ -0,0 +1 @@
+- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587))
diff --git a/changelog.d/features/10662-systemd-notify.md b/changelog.d/features/10662-systemd-notify.md
new file mode 100644
index 000000000000..5a02e7df2528
--- /dev/null
+++ b/changelog.d/features/10662-systemd-notify.md
@@ -0,0 +1 @@
+- feat(server): emit systemd sd_notify READY/WATCHDOG/STOPPING (generated unit becomes Type=notify with WatchdogSec=180) so a frozen server process is killed and restarted by systemd instead of lingering undetected
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 000000000000..1ec6e5f0b728
--- /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/changelog.d/features/10670-call-logs-error-type.md b/changelog.d/features/10670-call-logs-error-type.md
new file mode 100644
index 000000000000..1ffd94bd2203
--- /dev/null
+++ b/changelog.d/features/10670-call-logs-error-type.md
@@ -0,0 +1 @@
+- **feat(call_logs):** persist the per-call error family in `call_logs.error_type` and expose a failure breakdown (`errorBreakdown`) in the usage analytics endpoint, reusing the existing production classifier ([#10670](https://github.com/diegosouzapw/OmniRoute/issues/10670))
diff --git a/changelog.d/features/10677-egress-sharing-summary.md b/changelog.d/features/10677-egress-sharing-summary.md
new file mode 100644
index 000000000000..9e1f723a0d43
--- /dev/null
+++ b/changelog.d/features/10677-egress-sharing-summary.md
@@ -0,0 +1 @@
+- **feat(proxy):** the proxy-health sweep and `GET /api/settings/proxies/egress` now report an anonymous summary of egress-IP sharing — how many rotation groups share an egress IP and the largest number of accounts behind one IP — computed from persisted `proxy_logs` over a 24h window. No IPs and no account identities by default; `PROXY_LOG_INCLUDE_IPS=true` restores raw details. ([#10677](https://github.com/diegosouzapw/OmniRoute/issues/10677))
diff --git a/changelog.d/features/10729-cursor-api-key-and-cli-passthrough.md b/changelog.d/features/10729-cursor-api-key-and-cli-passthrough.md
new file mode 100644
index 000000000000..96094ffff17f
--- /dev/null
+++ b/changelog.d/features/10729-cursor-api-key-and-cli-passthrough.md
@@ -0,0 +1 @@
+- **feat(providers):** new `cursor-api` provider (card "Cursor API", alias `cua`): connect a Cursor user API key (`crsr_…`) and route `cursor-api/` through the existing Cursor agent executor (the key is exchanged for a 1h session token and cached), plus a `/api/cursor-cli/*` passthrough so the Cursor CLI itself runs through OmniRoute (`CURSOR_API_ENDPOINT=http:///api/cursor-cli`, `CURSOR_API_KEY=`) with every RPC attributed and logged. The IDE `cursor` provider is unchanged. (#10729)
diff --git a/changelog.d/features/10771-health-root-endpoint.md b/changelog.d/features/10771-health-root-endpoint.md
new file mode 100644
index 000000000000..a367bbce7812
--- /dev/null
+++ b/changelog.d/features/10771-health-root-endpoint.md
@@ -0,0 +1 @@
+- **feat(api):** `GET /api/health` now answers `{ status, timestamp }` without a key. Until now the path had no route, so the management-auth boundary answered first with a 401 — indistinguishable from a wrong key or an unknown route, which left Docker HEALTHCHECKs and Kubernetes probes unable to tell "down" from "misconfigured". Kept deliberately minimal: version, uptime and memory stay behind the authenticated `/api/monitoring/health` ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10771)).
diff --git a/changelog.d/features/10783-task-routing-configurable-patterns.md b/changelog.d/features/10783-task-routing-configurable-patterns.md
new file mode 100644
index 000000000000..e5c37c390ad4
--- /dev/null
+++ b/changelog.d/features/10783-task-routing-configurable-patterns.md
@@ -0,0 +1 @@
+- feat(routing): make Task-Aware Smart Routing's detection patterns operator-configurable via `settings.taskRouting.patternOverrides` (`PUT /api/settings/task-routing`) — the built-in patterns are English-only, so a non-English dashboard had no recourse short of turning detection off entirely; an override now replaces the pattern list for one task type without touching the rest (#10783)
diff --git a/changelog.d/features/10869-combo-patch-verb.md b/changelog.d/features/10869-combo-patch-verb.md
new file mode 100644
index 000000000000..f11893d95cd0
--- /dev/null
+++ b/changelog.d/features/10869-combo-patch-verb.md
@@ -0,0 +1 @@
+- feat(api): accept PATCH on /api/combos/[id], the verb the OpenAPI spec already documents (#10869)
diff --git a/changelog.d/features/10896-glm-5.3.md b/changelog.d/features/10896-glm-5.3.md
new file mode 100644
index 000000000000..0edfc4a55b24
--- /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/changelog.d/features/10897-home-recent-requests.md b/changelog.d/features/10897-home-recent-requests.md
new file mode 100644
index 000000000000..fd6bcc9abe47
--- /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/changelog.d/features/8443-credential-health-per-connection-interval.md b/changelog.d/features/8443-credential-health-per-connection-interval.md
new file mode 100644
index 000000000000..1fd1d3e5a228
--- /dev/null
+++ b/changelog.d/features/8443-credential-health-per-connection-interval.md
@@ -0,0 +1,2 @@
+- **feat(credential-health):** pace the credential health sweep per connection via `provider_connections.healthCheckInterval` (minutes, 0 = never), with `CREDENTIAL_HEALTH_CHECK_INTERVAL` as the global default ([#8443](https://github.com/diegosouzapw/OmniRoute/issues/8443))
+- **behavior change:** `healthCheckInterval` is a shared column — it paces both the OAuth token refresh and the credential health sweep, and `0` disables both. The connection editor defaults it to 60, so configured OAuth connections are now credential-checked at 60min instead of the previous ~10min (aligned with the probe-volume goal of #8443)
diff --git a/changelog.d/features/command-code-reasoning-efforts.md b/changelog.d/features/command-code-reasoning-efforts.md
new file mode 100644
index 000000000000..3e9b172204a4
--- /dev/null
+++ b/changelog.d/features/command-code-reasoning-efforts.md
@@ -0,0 +1 @@
+- feat(command-code): advertise low/medium/high/xhigh/max reasoning-effort suffixes for reasoning-capable models in the catalog and Combo Builder, with request-time resolution to reasoning_effort
diff --git a/changelog.d/features/cursor-agent-image-provider.md b/changelog.d/features/cursor-agent-image-provider.md
new file mode 100644
index 000000000000..84646dc44ebe
--- /dev/null
+++ b/changelog.d/features/cursor-agent-image-provider.md
@@ -0,0 +1 @@
+- feat(sse): add Cursor plan image generation via Agent CLI (`IMAGE_PROVIDERS.cursor`, format `cursor-agent-image`), reusing the chat Cursor OAuth connection
diff --git a/changelog.d/features/disable-context-window-checks.md b/changelog.d/features/disable-context-window-checks.md
new file mode 100644
index 000000000000..1cdd3cc0a8bb
--- /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/changelog.d/features/kimi-coding-extra-usage.md b/changelog.d/features/kimi-coding-extra-usage.md
new file mode 100644
index 000000000000..766ec1020cf6
--- /dev/null
+++ b/changelog.d/features/kimi-coding-extra-usage.md
@@ -0,0 +1 @@
+- **feat(usage):** show Kimi Coding's fixed-order Code 5-hour/7-day quota windows plus Extra Usage status, balance, monthly spend/limit, and the official Additional Credits link on Dashboard → Quota cards.
diff --git a/changelog.d/features/opencode-go-muse-spark-efforts.md b/changelog.d/features/opencode-go-muse-spark-efforts.md
new file mode 100644
index 000000000000..25da8482a935
--- /dev/null
+++ b/changelog.d/features/opencode-go-muse-spark-efforts.md
@@ -0,0 +1 @@
+- feat(opencode-go): expose Muse Spark 1.2 Contributor reasoning-effort aliases (minimal/low/medium/high/xhigh) in the Combo Builder
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 000000000000..579005e94357
--- /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/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md
new file mode 100644
index 000000000000..7a976ab85dd7
--- /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/changelog.d/fixes/10162-approximate-combo-context-advisory.md b/changelog.d/fixes/10162-approximate-combo-context-advisory.md
new file mode 100644
index 000000000000..3c1bc703a082
--- /dev/null
+++ b/changelog.d/fixes/10162-approximate-combo-context-advisory.md
@@ -0,0 +1 @@
+- **fix(routing):** keep approximate Combo context estimates advisory so requests reach concrete targets instead of returning a pre-dispatch 400 ([#10162](https://github.com/diegosouzapw/OmniRoute/pull/10162)) — thanks @xz-dev
diff --git a/changelog.d/fixes/10345-bare-combo-opencode-ids.md b/changelog.d/fixes/10345-bare-combo-opencode-ids.md
new file mode 100644
index 000000000000..c3a6a499ecaf
--- /dev/null
+++ b/changelog.d/fixes/10345-bare-combo-opencode-ids.md
@@ -0,0 +1 @@
+- **fix(opencode-plugin):** publish bare combo model ids without the plugin provider prefix so OpenCode can select them ([#10345](https://github.com/diegosouzapw/OmniRoute/issues/10345))
diff --git a/changelog.d/fixes/10346-empty-pool-warn-once.md b/changelog.d/fixes/10346-empty-pool-warn-once.md
new file mode 100644
index 000000000000..e4b50ef3ffe0
--- /dev/null
+++ b/changelog.d/fixes/10346-empty-pool-warn-once.md
@@ -0,0 +1 @@
+- **fix(backend):** log `auto/ matched no connected models` once per process per label instead of every minute ([#10346](https://github.com/diegosouzapw/OmniRoute/issues/10346))
diff --git a/changelog.d/fixes/10353-memory-heap-conflict-warn.md b/changelog.d/fixes/10353-memory-heap-conflict-warn.md
new file mode 100644
index 000000000000..c52b7cc15c48
--- /dev/null
+++ b/changelog.d/fixes/10353-memory-heap-conflict-warn.md
@@ -0,0 +1 @@
+- **fix(docker):** warn at boot when `OMNIROUTE_MEMORY_MB` disagrees with `NODE_OPTIONS --max-old-space-size`, and document that the standalone/Docker launcher appends `OMNIROUTE_MEMORY_MB` last ([#10353](https://github.com/diegosouzapw/OmniRoute/issues/10353))
diff --git a/changelog.d/fixes/10470-antigravity-byop-account-rotation.md b/changelog.d/fixes/10470-antigravity-byop-account-rotation.md
new file mode 100644
index 000000000000..9ec58e152ad5
--- /dev/null
+++ b/changelog.d/fixes/10470-antigravity-byop-account-rotation.md
@@ -0,0 +1 @@
+- **fix(antigravity):** automatically rotate to a sibling account when one is BYOP (GCP Project ID required, `gcp_project_required` 422) — the account is excluded from selection for 24h and the request succeeds via another account instead of failing fast; the actionable 422 is surfaced only when no sibling exists (follow-up to the #10424 BYOP fast-fail) ([#10470](https://github.com/diegosouzapw/OmniRoute/pull/10470)) — thanks @rqzbeh
diff --git a/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 000000000000..9354b028228c
--- /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/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 000000000000..11a1845715e4
--- /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/changelog.d/fixes/10550-responses-reasoning-transport.md b/changelog.d/fixes/10550-responses-reasoning-transport.md
new file mode 100644
index 000000000000..d34c433debd8
--- /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/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md
new file mode 100644
index 000000000000..ca602b912245
--- /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/changelog.d/fixes/10594-freepik-magnific-api.md b/changelog.d/fixes/10594-freepik-magnific-api.md
new file mode 100644
index 000000000000..4c1c59a70165
--- /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/changelog.d/fixes/10597-combo-log-error-body.md b/changelog.d/fixes/10597-combo-log-error-body.md
new file mode 100644
index 000000000000..ff6608947ccc
--- /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/changelog.d/fixes/10686-combo-quota-token-limit-await.md b/changelog.d/fixes/10686-combo-quota-token-limit-await.md
new file mode 100644
index 000000000000..a9e7b910e3b5
--- /dev/null
+++ b/changelog.d/fixes/10686-combo-quota-token-limit-await.md
@@ -0,0 +1 @@
+- **Combo routing:** await each connection's token limit before reserving quota. The old lookup treated the `Promise` as a connection and dropped `rateLimitOverrides.tpm` ([#10686](https://github.com/diegosouzapw/OmniRoute/pull/10686)).
diff --git a/changelog.d/fixes/10732-copilot-m365-invocation-refresh.md b/changelog.d/fixes/10732-copilot-m365-invocation-refresh.md
new file mode 100644
index 000000000000..acbfbbe693de
--- /dev/null
+++ b/changelog.d/fixes/10732-copilot-m365-invocation-refresh.md
@@ -0,0 +1 @@
+- **fix(providers):** copilot-m365-web chat turns no longer surface as `(empty response)` — the type:4 invocation is aligned with the 2026-08 wire shape and now carries its type:1 Metrics follow-up in the same socket write, and the access token pre-flight-refreshes from a stored refresh_token instead of requiring a DevTools re-capture every ~75 minutes ([#10732](https://github.com/diegosouzapw/OmniRoute/pull/10732) — thanks @acc0mplish)
diff --git a/changelog.d/fixes/10734-combo-context-generic-default.md b/changelog.d/fixes/10734-combo-context-generic-default.md
new file mode 100644
index 000000000000..988c435d4e36
--- /dev/null
+++ b/changelog.d/fixes/10734-combo-context-generic-default.md
@@ -0,0 +1 @@
+- **fix(catalog):** stop counting `getTokenLimit()`'s generic 128k catch-all as a known combo window, so `/v1/models` advertises the min of sourced member contexts instead of collapsing a 500k combo to 128k ([#10734](https://github.com/diegosouzapw/OmniRoute/issues/10734))
diff --git a/changelog.d/fixes/10735-search-provider-named-errors.md b/changelog.d/fixes/10735-search-provider-named-errors.md
new file mode 100644
index 000000000000..0e82f36aa87b
--- /dev/null
+++ b/changelog.d/fixes/10735-search-provider-named-errors.md
@@ -0,0 +1 @@
+- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735))
diff --git a/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md
new file mode 100644
index 000000000000..ff36462d4704
--- /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/changelog.d/fixes/10769-cache-stats-real-cache.md b/changelog.d/fixes/10769-cache-stats-real-cache.md
new file mode 100644
index 000000000000..baaacc660d4f
--- /dev/null
+++ b/changelog.d/fixes/10769-cache-stats-real-cache.md
@@ -0,0 +1 @@
+- **fix(api):** `/api/cache/stats` reported the prompt-cache LRU, which no request path ever writes to — it answered `0 hit / 0 miss, size 0` while the semantic cache served real traffic, and the Health and Usage dashboards rendered that as fact. It now reports the semantic cache's in-memory entries, with the same response shape ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10769)) — thanks @Poid-ZA, who first fixed this in #9446.
diff --git a/changelog.d/fixes/10770-console-interceptor-message-fidelity.md b/changelog.d/fixes/10770-console-interceptor-message-fidelity.md
new file mode 100644
index 000000000000..c35260f36a7d
--- /dev/null
+++ b/changelog.d/fixes/10770-console-interceptor-message-fidelity.md
@@ -0,0 +1 @@
+- **fix(logging):** the app log is filterable and readable again. Entries from the tagged logger (`[LEVEL] [TAG] message`) were filed under the level instead of the component, and printf format strings were never applied, so `%s`/`%d` stayed literal with the values trailing behind them unlabelled — including every LiveWS connection line, where the format is deliberate hardening against injected format specifiers ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10770)).
diff --git a/changelog.d/fixes/10774-claude-code-flat-rate.md b/changelog.d/fixes/10774-claude-code-flat-rate.md
new file mode 100644
index 000000000000..ea09e2b208a4
--- /dev/null
+++ b/changelog.d/fixes/10774-claude-code-flat-rate.md
@@ -0,0 +1 @@
+- **fix(analytics):** Claude Code (`claude`/`cc`) is a flat-rate subscription, so cost analytics reports `$0` for it instead of estimating Anthropic list prices — the metered `anthropic` API keeps its real cost, and budget/quota/routing still estimate as before ([#10774](https://github.com/diegosouzapw/OmniRoute/pull/10774)) — thanks @electrumguy
diff --git a/changelog.d/fixes/10781-wal-truncate-scheduler.md b/changelog.d/fixes/10781-wal-truncate-scheduler.md
new file mode 100644
index 000000000000..4eb13a271bbb
--- /dev/null
+++ b/changelog.d/fixes/10781-wal-truncate-scheduler.md
@@ -0,0 +1 @@
+- fix(db): periodically run `wal_checkpoint(TRUNCATE)` so the SQLite WAL file shrinks on long-running servers (default 6h, override with `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS`, `0` disables) (#10781)
diff --git a/changelog.d/fixes/10782-ws-heartbeat-ping-pong.md b/changelog.d/fixes/10782-ws-heartbeat-ping-pong.md
new file mode 100644
index 000000000000..23aeaf3d3a38
--- /dev/null
+++ b/changelog.d/fixes/10782-ws-heartbeat-ping-pong.md
@@ -0,0 +1 @@
+- fix(sse): replace LiveWS's application-only liveness check with a protocol-level `ws.ping()`/`pong` heartbeat (RFC 6455 §5.5.2) alongside the existing one, so a read-only dashboard subscriber that never sends anything survives the connection timeout — a socket that stops reading frames entirely is still reaped exactly as before (#10782)
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 000000000000..0437576d38b8
--- /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/changelog.d/fixes/10792-double-transport-retry-scope.md b/changelog.d/fixes/10792-double-transport-retry-scope.md
new file mode 100644
index 000000000000..337b680add06
--- /dev/null
+++ b/changelog.d/fixes/10792-double-transport-retry-scope.md
@@ -0,0 +1 @@
+- **fix(resilience):** scope the same-account transport retry (#9708) out of emergency-fallback and combo hops — it was retrying the free fallback model and combo targets too, doubling upstream calls and corrupting the terminal error status on those paths.
diff --git a/changelog.d/fixes/10798-respect-log-level-provider-catalog.md b/changelog.d/fixes/10798-respect-log-level-provider-catalog.md
new file mode 100644
index 000000000000..3a11aab90932
--- /dev/null
+++ b/changelog.d/fixes/10798-respect-log-level-provider-catalog.md
@@ -0,0 +1 @@
+- **fix(opencode-plugin):** respect log level in provider.models() catalog path so debug/info/warn messages are suppressed when `features.logLevel` is set to `"error"` ([#10798](https://github.com/diegosouzapw/OmniRoute/pull/10798)) — thanks @tientien17
diff --git a/changelog.d/fixes/10799-provider-health-inconclusive-probes.md b/changelog.d/fixes/10799-provider-health-inconclusive-probes.md
new file mode 100644
index 000000000000..72aacacee027
--- /dev/null
+++ b/changelog.d/fixes/10799-provider-health-inconclusive-probes.md
@@ -0,0 +1 @@
+- **fix(providers):** Keep NVIDIA timeout probes and generic Antigravity/AGY HTTP 400 probes from poisoning credential health while preserving explicit Google geo-block handling ([#10799](https://github.com/diegosouzapw/OmniRoute/pull/10799)) — thanks @Zartharas
diff --git a/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md
new file mode 100644
index 000000000000..51768aab4c1e
--- /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/changelog.d/fixes/10832-unprefixed-dalle3.md b/changelog.d/fixes/10832-unprefixed-dalle3.md
new file mode 100644
index 000000000000..2dfd970b13c9
--- /dev/null
+++ b/changelog.d/fixes/10832-unprefixed-dalle3.md
@@ -0,0 +1 @@
+- **fix(images):** register OpenAI `dall-e-3` in the image registry so unprefixed `dall-e-3` (and `openai/dall-e-3`) route to OpenAI Images instead of Microsoft Designer Web, and so the chat catalog no longer lists `openai/dall-e-3` as a 128k chat model ([#10832](https://github.com/diegosouzapw/OmniRoute/issues/10832))
diff --git a/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md b/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md
new file mode 100644
index 000000000000..2894ff65b027
--- /dev/null
+++ b/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md
@@ -0,0 +1 @@
+- **fix(security):** Outbound URL guard now resolves IPv4-mapped IPv6 literals to their embedded address, so `[::ffff:169.254.169.254]` is refused by the unconditional cloud-metadata block like its dotted spelling; `[::]` is refused alongside `0.0.0.0` ([#10843](https://github.com/diegosouzapw/OmniRoute/pull/10843)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10848-image-scan-cookie-bridge.md b/changelog.d/fixes/10848-image-scan-cookie-bridge.md
new file mode 100644
index 000000000000..07e0f2029160
--- /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/changelog.d/fixes/10849-search-provider-opaque-400.md b/changelog.d/fixes/10849-search-provider-opaque-400.md
new file mode 100644
index 000000000000..a8982fb19436
--- /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/changelog.d/fixes/10853-i18n-disabled-mistranslation.md b/changelog.d/fixes/10853-i18n-disabled-mistranslation.md
new file mode 100644
index 000000000000..836cc6491e01
--- /dev/null
+++ b/changelog.d/fixes/10853-i18n-disabled-mistranslation.md
@@ -0,0 +1 @@
+- **fix(i18n):** The "Disabled" status no longer renders as the noun for a person with a disability in Japanese, Spanish, Hindi, Polish, Telugu, Urdu and both Chinese locales — 24 strings now use each catalog's existing wording (ja 無効, es Deshabilitado, hi अक्षम, pl Wyłączone, te నిలిపివేయబడింది, ur غیر فعال, zh-CN 已禁用, zh-TW 已停用) ([#10812](https://github.com/diegosouzapw/OmniRoute/issues/10812), [#10853](https://github.com/diegosouzapw/OmniRoute/pull/10853)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10854-skills-marketplace-owner.md b/changelog.d/fixes/10854-skills-marketplace-owner.md
new file mode 100644
index 000000000000..e80a109f77fa
--- /dev/null
+++ b/changelog.d/fixes/10854-skills-marketplace-owner.md
@@ -0,0 +1 @@
+- **fix(skills):** Marketplace-installed skills are available to API-key-scoped requests, including existing SkillsMP and skills.sh installs ([#10854](https://github.com/diegosouzapw/OmniRoute/pull/10854)) — thanks @kriptoburak
diff --git a/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md
new file mode 100644
index 000000000000..39da596ee37e
--- /dev/null
+++ b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md
@@ -0,0 +1 @@
+- **fix(catalog):** `/v1/models` no longer advertises the built-in `auto/*` ids while auto routing is disabled — they were listed but rejected at request time with `Auto routing is disabled` ([#10831](https://github.com/diegosouzapw/OmniRoute/issues/10831), [#10857](https://github.com/diegosouzapw/OmniRoute/pull/10857)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10858-base64-file-token-estimate.md b/changelog.d/fixes/10858-base64-file-token-estimate.md
new file mode 100644
index 000000000000..18d8104b107e
--- /dev/null
+++ b/changelog.d/fixes/10858-base64-file-token-estimate.md
@@ -0,0 +1 @@
+- **fix(context):** Base64 file payloads (OpenAI `file` parts, Responses `input_file`, Claude `document` blocks) are budgeted like the Gemini `inlineData` path instead of being counted as prompt text — a ~1MB PDF estimated at 350k tokens and was rejected on the context limit before reaching the provider's document pipeline ([#10840](https://github.com/diegosouzapw/OmniRoute/issues/10840), [#10858](https://github.com/diegosouzapw/OmniRoute/pull/10858)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md b/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md
new file mode 100644
index 000000000000..a23aeed2a17b
--- /dev/null
+++ b/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md
@@ -0,0 +1 @@
+- **fix(mcp):** MCP tool calls that wait on a model provider no longer abort after 10 seconds. `omniRouteFetch` applied a single hardcoded `AbortSignal.timeout(10000)` to every internal hop, and `omniroute_route_request` — which posts to `/v1/chat/completions` and waits on the upstream provider, plus auto-combo candidate probing before a provider is even chosen — passed no signal of its own, so it inherited it. Any route slower than 10s failed from the MCP side while the identical request succeeded through the REST API. `omniroute_web_search` and `omniroute_web_fetch` in the same file already carried an explicit 60s signal, so that value is now shared by all three provider-bound calls instead of being repeated as a literal, while management reads (health, resilience, rate limits, combos, quota, usage) keep their fast-fail 10s budget so a stalled local endpoint still cannot hold a tool call open. Both budgets are overridable through `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` and `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS`, replacing the reported workaround of patching the compiled `dist/.build/next/server/chunks/*.js`; a malformed or non-positive override falls back to the default rather than disabling the timeout
diff --git a/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md b/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md
new file mode 100644
index 000000000000..fd6f7d3f04ee
--- /dev/null
+++ b/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md
@@ -0,0 +1 @@
+- **fix(providers):** importing models with an expired API key now surfaces the credential error instead of reporting "No new models were added". The Import button posts to `/api/providers/{id}/sync-models`, which self-fetches the models route; that route does not fail on an upstream 401 but degrades to a catalog it already has, preferring the cache and using the local catalog only when there is no cache. A provider that imported successfully once therefore has a cache, so an expired key produced `{ source: "cache", warning: "Models probe failed (401) — using cached catalog" }` with HTTP 200 — and the #5460/#5465 degradation guard only recognised the `local_catalog` branch, so model-sync accepted it as a successful discovery, found every cached model already imported, and returned the empty-diff result. Retest does not go through this path, which is why it failed correctly and made the import look like a genuine "nothing to do". The existing rule — a degraded discovery must not be persisted as the synced catalog — is now applied to the branch it missed rather than special-casing 401/403, discriminating on the warning the fallback builder always attaches (an ordinary non-refresh cache hit attaches none, and model-sync always requests `refresh=true`). `isDegradedLocalCatalog` keeps its exact meaning and its existing tests
diff --git a/changelog.d/fixes/10866-combo-empty-models.md b/changelog.d/fixes/10866-combo-empty-models.md
new file mode 100644
index 000000000000..e71092d5d4cc
--- /dev/null
+++ b/changelog.d/fixes/10866-combo-empty-models.md
@@ -0,0 +1 @@
+- fix(api): reject a combo update that removes every model, and store the copilot's combo targets where the router reads them (#10866)
diff --git a/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md b/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md
new file mode 100644
index 000000000000..91f4e717e8d6
--- /dev/null
+++ b/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md
@@ -0,0 +1 @@
+- **fix(proxy):** proxy "Test connection" no longer reports an IPv4-only SOCKS5/SSH proxy as dead. #1255 moved every egress probe from `api.ipify.org` to `api64.ipify.org` so proxies with IPv6 egress could be tested, but `api64` is IPv6-first: a tunnel with no IPv6 route has nothing to connect to, so the probe hung until the caller's deadline and a proxy that was carrying live LLM traffic came back as a failure. Swapping the target to `api4` fixes that case and re-breaks the one #1255 fixed, so the probe now tries the targets in order instead — `api64` first, so a proxy with working IPv6 answers on the first attempt and keeps the exact behaviour #1255 introduced, including which of its addresses is reported (the egress IP is used as an identity to detect accounts of one rotation group sharing an address, so the attempts are sequential rather than raced). The attempts split the budget each call site already enforced, so no probe can take longer than it could before, and each attempt gets its own `AbortController` so exhausting the budget on an unreachable target does not abort the next one. `OMNIROUTE_PROXY_ECHO_URL` pins a single target — including a self-hosted echo — replacing the workaround of rewriting the compiled bundle after every upgrade. The relay branch of the test route still targets `api64` through `x-relay-target`, since that request egresses from the relay worker rather than the operator's tunnel
diff --git a/changelog.d/fixes/10870-cli-env-collision.md b/changelog.d/fixes/10870-cli-env-collision.md
new file mode 100644
index 000000000000..95a428ba08b4
--- /dev/null
+++ b/changelog.d/fixes/10870-cli-env-collision.md
@@ -0,0 +1 @@
+- fix(cli): warn when a .env line never takes effect, and stop swallowing an unreadable .env (#10870)
diff --git a/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md b/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md
new file mode 100644
index 000000000000..44443eac6cd7
--- /dev/null
+++ b/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md
@@ -0,0 +1 @@
+- **fix(db):** Remove stale MiMoCode provider configuration, including the legacy `mcode` alias, left after provider retirement while preserving historical usage and call logs ([#10873](https://github.com/diegosouzapw/OmniRoute/pull/10873)) — thanks @Zartharas
diff --git a/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 000000000000..9501c6dad2a4
--- /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/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md b/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md
new file mode 100644
index 000000000000..1fc7c01933b0
--- /dev/null
+++ b/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md
@@ -0,0 +1 @@
+- **fix(provider-health):** Keep unsupported 404/405 validation probes neutral so they do not poison stored credential health or scheduler failure state, while still honoring per-connection health-check pacing ([#10878](https://github.com/diegosouzapw/OmniRoute/pull/10878)) — thanks @Zartharas
diff --git a/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md b/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md
new file mode 100644
index 000000000000..b1ff4bbf5a0c
--- /dev/null
+++ b/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md
@@ -0,0 +1 @@
+- **fix(antigravity):** map Gemini 3.7 Flash tier ids (`gemini-3.7-flash-high/medium/low`, bare `gemini-3.7-flash`) to the upstream `gemini-3.7-flash-tiered` model id Google's Cloud Code endpoint expects, and configure per-tier thinking budgets ([#10882](https://github.com/diegosouzapw/OmniRoute/pull/10882)) — thanks @adevwithpurpose
diff --git a/changelog.d/fixes/10887-memory-mcp-tools.md b/changelog.d/fixes/10887-memory-mcp-tools.md
new file mode 100644
index 000000000000..8dc02db1d9bc
--- /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/changelog.d/fixes/10902-pplx-search-hint-optin.md b/changelog.d/fixes/10902-pplx-search-hint-optin.md
new file mode 100644
index 000000000000..233fb61f1984
--- /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/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md b/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md
new file mode 100644
index 000000000000..bd4a70a1ab59
--- /dev/null
+++ b/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md
@@ -0,0 +1 @@
+- **fix(images):** retry Codex image generation on a sibling ChatGPT account when the requested model isn't entitled on the current account, instead of failing the request outright ([#8307](https://github.com/diegosouzapw/OmniRoute/pull/8307)).
diff --git a/changelog.d/fixes/9692-openai-to-claude-tool-images.md b/changelog.d/fixes/9692-openai-to-claude-tool-images.md
new file mode 100644
index 000000000000..c082d0dbc3a6
--- /dev/null
+++ b/changelog.d/fixes/9692-openai-to-claude-tool-images.md
@@ -0,0 +1 @@
+- **fix(translator):** convert OpenAI `image_url` blocks nested in `role: "tool"` / `tool_result` content to Claude `image` source blocks so OpenAI-compatible clients (Kimi Code CLI `ReadMediaFile`, and any other tool that returns media) no longer 400 the next Claude-format upstream turn ([#9692](https://github.com/diegosouzapw/OmniRoute/issues/9692))
diff --git a/changelog.d/fixes/9708-codex-same-account-retry.md b/changelog.d/fixes/9708-codex-same-account-retry.md
new file mode 100644
index 000000000000..2ccb7fb97fc5
--- /dev/null
+++ b/changelog.d/fixes/9708-codex-same-account-retry.md
@@ -0,0 +1 @@
+- **fix(resilience):** retry a retryable Codex pre-output 502/503/504/507 once on the same account (2–3s jitter) before cooling the connection, and stop translating that mixed pool into an all-accounts quota `429` ([#9708](https://github.com/diegosouzapw/OmniRoute/issues/9708))
diff --git a/changelog.d/fixes/PENDING-electron-window-hidden-hostname-bind.md b/changelog.d/fixes/PENDING-electron-window-hidden-hostname-bind.md
new file mode 100644
index 000000000000..6a88ba926a62
--- /dev/null
+++ b/changelog.d/fixes/PENDING-electron-window-hidden-hostname-bind.md
@@ -0,0 +1 @@
+- **fix(electron):** desktop window stays hidden on Windows because the embedded Next.js server binds to the machine hostname instead of loopback ([#PENDING](https://github.com/diegosouzapw/OmniRoute/pull/PENDING))
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 000000000000..17483dce522d
--- /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/changelog.d/fixes/cline-task-id-passthrough.md b/changelog.d/fixes/cline-task-id-passthrough.md
new file mode 100644
index 000000000000..a6d2ecec57ca
--- /dev/null
+++ b/changelog.d/fixes/cline-task-id-passthrough.md
@@ -0,0 +1 @@
+- **fix(cline):** Preserve client-supplied Cline task IDs and omit the header when clients provide none, preventing request-scoped proxy IDs from being reported as tasks.
diff --git a/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md b/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md
new file mode 100644
index 000000000000..3a53f1323a95
--- /dev/null
+++ b/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md
@@ -0,0 +1 @@
+- **fix(catalog):** derive combo reasoning-effort tiers from the exact runtime-selectable connection scope, intersecting dynamic, pinned, allowlisted, and compatible provider-node evidence while failing closed on unknown capabilities.
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 000000000000..e0cf2114cab9
--- /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/changelog.d/maintenance/10317-latest-tracks-highest-stable.md b/changelog.d/maintenance/10317-latest-tracks-highest-stable.md
new file mode 100644
index 000000000000..9fdb4d2c81c7
--- /dev/null
+++ b/changelog.d/maintenance/10317-latest-tracks-highest-stable.md
@@ -0,0 +1 @@
+- **docs(docker):** spell out that `:latest` tracks the highest **published** stable SemVer (not git `main`), and that GitOps should pin `X.Y.Z` ([#10317](https://github.com/diegosouzapw/OmniRoute/issues/10317))
diff --git a/changelog.d/maintenance/10349-optional-work-event-loop.md b/changelog.d/maintenance/10349-optional-work-event-loop.md
new file mode 100644
index 000000000000..0cd695e4a7c4
--- /dev/null
+++ b/changelog.d/maintenance/10349-optional-work-event-loop.md
@@ -0,0 +1 @@
+- **docs(backend):** document that memory extraction, skills injection, and token refresh share the request event loop, plus dashboard kill switches ([#10349](https://github.com/diegosouzapw/OmniRoute/issues/10349))
diff --git a/changelog.d/maintenance/10350-sqlite-single-replica-ha.md b/changelog.d/maintenance/10350-sqlite-single-replica-ha.md
new file mode 100644
index 000000000000..9b158d5787b3
--- /dev/null
+++ b/changelog.d/maintenance/10350-sqlite-single-replica-ha.md
@@ -0,0 +1 @@
+- **docs(docker):** document default SQLite as single-replica / HA-unsupported, including Recreate and HEALTHCHECK session blast radius ([#10350](https://github.com/diegosouzapw/OmniRoute/issues/10350))
diff --git a/changelog.d/maintenance/10351-pre-write-backup-throttle.md b/changelog.d/maintenance/10351-pre-write-backup-throttle.md
new file mode 100644
index 000000000000..f6141d30eaf9
--- /dev/null
+++ b/changelog.d/maintenance/10351-pre-write-backup-throttle.md
@@ -0,0 +1 @@
+- **docs(backend):** document that pre-write SQLite backups (including models.dev pricing) are throttled to once per 60 minutes and can be disabled with `DISABLE_SQLITE_AUTO_BACKUP` ([#10351](https://github.com/diegosouzapw/OmniRoute/issues/10351))
diff --git a/changelog.d/maintenance/10775-remove-dead-enforce-secrets.md b/changelog.d/maintenance/10775-remove-dead-enforce-secrets.md
new file mode 100644
index 000000000000..4f44473360ba
--- /dev/null
+++ b/changelog.d/maintenance/10775-remove-dead-enforce-secrets.md
@@ -0,0 +1 @@
+- chore(security): remove the unused `enforceSecrets()` duplicate of the boot secret check and pin the live `enforceWebRuntimeEnv()` wiring with a regression test (#10775)
diff --git a/changelog.d/maintenance/10779-combo-invocation-docs.md b/changelog.d/maintenance/10779-combo-invocation-docs.md
new file mode 100644
index 000000000000..00138a3a65b9
--- /dev/null
+++ b/changelog.d/maintenance/10779-combo-invocation-docs.md
@@ -0,0 +1 @@
+- **docs:** Custom combos are only invoked by their exact name in the `model` field — `auto` remains a separate zero-config router, and `openrouter/auto` is a paid OpenRouter product, not an alias ([#10779](https://github.com/diegosouzapw/OmniRoute/pull/10779)) — thanks @maxmad64bis
diff --git a/changelog.d/maintenance/10780-server-init-dead-code.md b/changelog.d/maintenance/10780-server-init-dead-code.md
new file mode 100644
index 000000000000..ffe204a23316
--- /dev/null
+++ b/changelog.d/maintenance/10780-server-init-dead-code.md
@@ -0,0 +1 @@
+- chore(startup): remove `src/server-init.ts` (183 lines, never imported — the boot path is `src/instrumentation-node.ts`) and correct four `"called from server-init.ts"` comments left pointing at the dead entry point (#10780)
diff --git a/changelog.d/maintenance/10875-combos-id-verb-coverage.md b/changelog.d/maintenance/10875-combos-id-verb-coverage.md
new file mode 100644
index 000000000000..5610a7ea416a
--- /dev/null
+++ b/changelog.d/maintenance/10875-combos-id-verb-coverage.md
@@ -0,0 +1 @@
+- **docs(openapi):** document the `GET` and `PUT` operations on `/api/combos/{id}`, and add an operation-level coverage floor so a missing verb can no longer hide behind a path that already counts as covered ([#10875](https://github.com/diegosouzapw/OmniRoute/pull/10875))
diff --git a/changelog.d/maintenance/7786-management-auth-guide.md b/changelog.d/maintenance/7786-management-auth-guide.md
new file mode 100644
index 000000000000..54f84a79b94e
--- /dev/null
+++ b/changelog.d/maintenance/7786-management-auth-guide.md
@@ -0,0 +1 @@
+- **docs(auth):** distinguish dashboard sessions, `oma_live_…` Access Tokens, manage-scoped API keys, and inference keys ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
diff --git a/changelog.d/maintenance/env-doc-sync-adhoc-bot.md b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md
new file mode 100644
index 000000000000..dbd759be295c
--- /dev/null
+++ b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md
@@ -0,0 +1 @@
+- **chore(ci):** ignore ad-hoc `BOT_TOKEN`/`BOT_URL` in env-doc-sync (scripts/ad-hoc mesh helpers, not runtime config)
diff --git a/changelog.d/maintenance/regen-translate-path-golden-freebuff.md b/changelog.d/maintenance/regen-translate-path-golden-freebuff.md
new file mode 100644
index 000000000000..7822df2283a7
--- /dev/null
+++ b/changelog.d/maintenance/regen-translate-path-golden-freebuff.md
@@ -0,0 +1 @@
+- chore(test): regenerate the provider/translate-path golden snapshot to reflect freebuff (#10531), fixing a base-red left by that merge (freebuff/freeinference key ordering only, no value changes).
diff --git a/changelog.d/maintenance/release-v3850-basereds-stream-utils-20260820.md b/changelog.d/maintenance/release-v3850-basereds-stream-utils-20260820.md
new file mode 100644
index 000000000000..98b62cd4480f
--- /dev/null
+++ b/changelog.d/maintenance/release-v3850-basereds-stream-utils-20260820.md
@@ -0,0 +1 @@
+- **fix(tests):** realign the two `stream-utils` passthrough cases that still asserted the pre-#10017 SSE framing — the event-boundary case declares the OpenAI Responses client format it actually exercises, and the metadata case now pins that surviving lines stay inside one event instead of expecting the `:`/`id:` control lines that #10473 stopped forwarding to every client format.
diff --git a/config/quality/dependency-allowlist.json b/config/quality/dependency-allowlist.json
index f276fc45d293..c4476f95d158 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/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json
index 8006cbb2f3dc..40f74e61f94f 100644
--- a/config/quality/eslint-suppressions.json
+++ b/config/quality/eslint-suppressions.json
@@ -54,11 +54,6 @@
"count": 3
}
},
- "open-sse/handlers/chatCore/codexFailover.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"open-sse/handlers/chatCore/comboContextCache.ts": {
"no-restricted-imports": {
"count": 1
@@ -1023,11 +1018,6 @@
"count": 1
}
},
- "src/lib/api/modelTestRunner.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/lib/api/proxyRegistryRouteHandlers.ts": {
"no-restricted-imports": {
"count": 1
@@ -1083,11 +1073,6 @@
"count": 1
}
},
- "src/lib/embeddings/service.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/lib/evals/runtime.ts": {
"no-restricted-imports": {
"count": 1
@@ -2934,11 +2919,6 @@
"count": 83
}
},
- "tests/unit/responses-parse-once-4041.test.ts": {
- "@typescript-eslint/no-explicit-any": {
- "count": 6
- }
- },
"tests/unit/responses-translation-fixes.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 35
diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index 96001706ef3c..72bc03087541 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -1,4 +1,6 @@
{
+ "_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).",
"_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.)",
@@ -304,6 +306,8 @@
"_rebaseline_2026_07_27_3850_relax_filesize_cap": "OWNER-APPROVED TEMPORARY relax for v3.8.50-3.8.54 PREPARE phase (docs/ROADMAP.md). cap 800->900 (+100), testCap 800->900 (+100). Targets: decompose-existing-frozen unchanged (frozen still only-shrink); this only relaxes the cap for NEW files in the decompose/extract-while-PREPARE phase (.51='executor registry in-place' and .52='combo.ts decomposition' create new leaf modules above 800). RE-TIGHTENING MANDATORY in v3.8.51: cap target 850 = 850 once decomposition wave stabilizes. SUPERSEDED by _rebaseline_2026_07_27_3850_relax_filesize_cap_v2_20pct (v1 +20% buffer) — retained for audit. Tracked via same roadmap issue.",
"_rebaseline_2026_07_27_v3849_train1h": "Merge-train 1H (31 PRs) — owner-approved 2026-07-27. Two distinct causes, kept separate on purpose: (1) GENUINE irreducible growth at existing chokepoints — providerLimits/auth (#8632 Kimi quota-reset recovery), rateLimitManager (#8616 idle wedged limiters), models-catalog-route.test (#8610 OpenCode Go effort aliases); (2) COLLISION with #8585, which banked shrinks measured on the pre-train release tip while 30 sibling PRs in the SAME train grew those files again — chat/accountFallback (#8628), chatCore (#8613), videoGeneration (#8581), imageGeneration. The zero-headroom frozen entries cannot absorb either. Ceilings re-pinned to the post-merge tip; #8612 (also in this train) automates shrink-banking so this self-inflicted drift stops recurring. Detail: src/lib/usage/providerLimits.ts 1006->1013 (#8632); src/sse/services/auth.ts 2492->2508 (#8632); open-sse/services/rateLimitManager.ts 1014->1060 (#8616); src/sse/handlers/chat.ts 1842->1845 (#8628); open-sse/handlers/chatCore.ts 4939->4955 (#8613); open-sse/handlers/imageGeneration.ts 3100->3101 ((sem PR — teto do #8585)); open-sse/handlers/videoGeneration.ts 1038->1063 (#8581); open-sse/services/accountFallback.ts 1965->1966 (#8628); tests/unit/models-catalog-route.test.ts 1608->1636 (#8610)",
"frozen": {
+ "_rebaseline_2026_08_20_10878_10799_provider_health_probes": "PRs #10878 (unsupported OpenAI-like validation probes stay neutral) + #10799 (preserve credential health on inconclusive NVIDIA-timeout/Antigravity-400 probes) own growth: src/app/api/providers/[id]/test/route.ts 946->1025 (+79, sum of both boarded together). Both add narrowly-scoped classification branches at the existing test-route dispatch chokepoint (unsupported-capability skip, credential-inconclusive detection) rather than new files, mirroring the prior 2026_06_27_5193 rebaseline of the same file. Covered by tests/unit/provider-validation-unsupported-neutral.test.ts + tests/unit/provider-health-inconclusive-probes.test.ts.",
+ "src/app/api/providers/[id]/test/route.ts": 1025,
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, 1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
"src/lib/modelCapabilities.ts": 1006,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
- "open-sse/config/imageRegistry.ts": 1019,
- "src/sse/handlers/chatHelpers.ts": 1017,
- "src/shared/middleware/chatBodyAdmission.ts": 1005
+ "open-sse/config/imageRegistry.ts": 1034,
+ "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)."
},
"_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).",
@@ -615,5 +620,7 @@
"_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.",
"_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).",
"_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).",
- "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session)."
+ "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session).",
+ "_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.",
+ "_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature."
}
\ No newline at end of file
diff --git a/config/quality/open-sse-typecheck-baseline.json b/config/quality/open-sse-typecheck-baseline.json
index 9b900ce2bd53..c6de98b41840 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/config/quality/quality-baseline.json b/config/quality/quality-baseline.json
index c8cbd07d9458..3bfef6d099c0 100644
--- a/config/quality/quality-baseline.json
+++ b/config/quality/quality-baseline.json
@@ -102,7 +102,7 @@
"_rebaseline_2026_07_28_v3849_release": "75.5 -> 99 (+23.5). Aperto EXIGIDO pelo modo --require-tighten do ratchet: a métrica melhorou de verdade no ciclo v3.8.49. A causa é o workflow assíncrono de tradução, que finalmente alcançou o denominador em EN — as rebaselines anteriores (v3.8.39/.44/.47) foram todas afrouxamentos registrando o atraso das traduções, e agora ele foi pago. O coletor SUBTRAI os placeholders (present - placeholder em scripts/quality/collect-metrics.mjs), então os 317 marcadores __MISSING__ que esta release introduziu para o drift de valor já estão descontados dos 99 — o número é honesto, não inflado por placeholder. Medido pelo collect-metrics do CI no run 30404226939."
},
"deadExports": {
- "value": 415,
+ "value": 418,
"direction": "down",
"_rebaseline_2026_08_09_v3850_post_sweep": "227 -> 230. Measured by npm run check:dead-code on the unmodified release/v3.8.50 tip 382449d593 during the mandatory --full-ci pre-flight. The +3 is inherited cycle drift from the authorized merge sweep; this repair adds no production exports. Rebaseline records the actual tip so ci.yml quality-gate can run, while structural cleanup remains separate debt.",
"_rebaseline_2026_07_01_v3843_release": "225->227 (+2). v3.8.43 cycle drift, surfaced in the Quality Ratchet job after eslintWarnings was rebaselined (check:dead-code runs there). 227 = measured by check:dead-code (knip) on the release tip 4635076eb. The 5 CI fixes add 0 dead exports: safeHttpHref in linkify.ts is module-local AND used (called by linkifyText); no new exports; test files are not scanned. Tighten via --update next cycle.",
@@ -112,7 +112,8 @@
"_rebaseline_2026_06_26_v3837_release": "343->345. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
"_rebaseline_2026_08_11_v3850_merge_storm": "230 -> 248. Own drift from the 2026-08-11 merge storm (99 PRs into release/v3.8.50 via authorized sweep): new providers/executors/handlers added dead exports that knip cannot see as used. Measured on the base-fix tip (7ca73697b0 + this repair PR). Owner authorized rebaseline (2026-08-11) — structural cleanup remains separate debt.",
"_rebaseline_2026_08_13_v3850_knip_bump": "248 -> 409. NOT code-added dead exports: dependabot bump #10043 (2026-08-13) upgraded knip 6.27.0 -> 6.32.x, and the new knip detects 162 MORE genuinely-unused exports (331 vs 169 deadExports) that 6.27 missed. DEAD_FILES unchanged (78). Reproduced identically on the clean release/v3.8.50 tip 266e39d3 with a fresh knip 6.32 node_modules — so every PR is born red on this gate until the tool change is absorbed. Owner authorized rebaseline (2026-08-13, via base-reds PR #10260). Structural cleanup of the 162 newly-surfaced dead exports remains separate debt.",
- "_rebaseline_2026_08_14_ocr_imagetotext_series": "OCR/image-to-text series (#10275/#10283/#10287/#10289/#10291): deadExports 409 -> 415. Each PR in the series adds public util/registry exports that are exercised by their unit tests but not yet by a second production caller — normalizeImageBuffer (imageNormalize), MISTRAL_PASSTHROUGH / AZURE_DI_TRANSFORMATION / getOcrTransformation (ocrRegistry), resolveOcrCredentials (v1/ocr route). They are the documented public surface of the new modules and are covered by tests; structural cleanup stays tracked in #3501."
+ "_rebaseline_2026_08_14_ocr_imagetotext_series": "OCR/image-to-text series (#10275/#10283/#10287/#10289/#10291): deadExports 409 -> 415. Each PR in the series adds public util/registry exports that are exercised by their unit tests but not yet by a second production caller — normalizeImageBuffer (imageNormalize), MISTRAL_PASSTHROUGH / AZURE_DI_TRANSFORMATION / getOcrTransformation (ocrRegistry), resolveOcrCredentials (v1/ocr route). They are the documented public surface of the new modules and are covered by tests; structural cleanup stays tracked in #3501.",
+ "_rebaseline_2026_08_20_pr_10798": "415 -> 418. Inherited cycle drift from parallel merges into release/v3.8.50 since the 2026-08-14 OCR-series rebaseline (3 more dead exports surfaced by knip 6.32). This PR (#10798, omniroute-plugin log-level fix) adds 0 production exports: it touches @omniroute/opencode-plugin (separate workspace, not scanned), changelog.d/, and scripts/check/check-env-doc-sync.mjs (array entries, not exports). The +3 is NOT from this PR; rebaselined so the gate runs while structural cleanup of the newly-surfaced dead exports remains separate debt."
},
"cognitiveComplexity": {
"value": 1223,
@@ -166,7 +167,8 @@
"dedicatedGate": true
},
"zizmorFindings": {
- "value": 190,
+ "value": 192,
+ "_rebaseline_2026_08_20_radar_export_workflow": "190 -> 192 (+2). Workflow novo `.github/workflows/radar-export.yml` (passo 10 do go-live do Radar: publica o export estável do catálogo como asset de release para o servidor privado baixar via RADAR_EXPORT_URL). Os +2 são unpinned-uses @vN: actions/checkout@v7 + actions/setup-node@v7 — a MESMA convenção deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só este violaria a convenção. O findings artipacked do checkout foi CORRIGIDO com `persist-credentials: false` (o job publica via GH_TOKEN em `gh release`, não usa a credencial do checkout). Nenhuma classe nova de template-injection / cache-poisoning / dangerous-triggers. Medido local com zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 191; +1 do delta conhecido do runner (ver _rebaseline_2026_07_28_ci_runner_delta: o runner enxerga 1 unpinned-uses @vN a mais que o devbox no mesmo commit; a baseline segue o runner) => 192.",
"_rebaseline_2026_07_20_aliasresolver_hook_split_7808": "175 -> 176 (+1). Companion to PR #7808 (CodeQL js/incomplete-url-substring-sanitization fix in bin/aliasResolver.mjs). The +1 is NOT caused by this PR's code changes (bin/* is not a workflow file) — it is a pre-existing drift that surfaced because the ratchet gate runs on this PR's CI: the zizmor scanner version on the GitHub runner gained a new rule (or extended an existing one) since the v3.8.49 baseline was seeded on 2026-07-17. Breakdown: the new finding is an unpinned-uses @vN class item on one of the existing workflows (same deliberate convention as _scanner_harden_workflows_2026_06_16 — @vN is intentional, SHA-pinning only this one would violate the convention). No new template-injection/artipacked/cache-poisoning/dangerous-triggers classes introduced. Measured by the Quality Gates (Extended) job on run 29713001401 = 176, baseline was 175. Note: by the time this landed on release/v3.8.49, the baseline was already at 176 via _rebaseline_2026_07_17_combo_recovery_hints — this entry is kept as historical record; no further bump applied.",
"_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.",
"direction": "down",
diff --git a/docs/architecture/ADAPTIVE_ROUTING.md b/docs/architecture/ADAPTIVE_ROUTING.md
new file mode 100644
index 000000000000..73bacfec3a3c
--- /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/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md
index dacf0d6f58ba..23099237dbae 100644
--- a/docs/architecture/CODEBASE_DOCUMENTATION.md
+++ b/docs/architecture/CODEBASE_DOCUMENTATION.md
@@ -89,7 +89,6 @@ src/
├── i18n/ Locale bundles
├── instrumentation.ts Next.js instrumentation hook
├── instrumentation-node.ts
-├── server-init.ts Process-level bootstrap (env, DB, jobs, sync)
└── proxy.ts Top-level proxy bootstrap helper
```
diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md
index f61131d59422..4d594180549b 100644
--- a/docs/architecture/REPOSITORY_MAP.md
+++ b/docs/architecture/REPOSITORY_MAP.md
@@ -133,7 +133,6 @@ src/
├── types/ # Shared TS type files
├── instrumentation.ts # Next.js telemetry hook (browser + edge)
├── instrumentation-node.ts # Node-only instrumentation
-├── server-init.ts # Server bootstrap (DB migrations, jobs, cleanup)
└── proxy.ts # HTTP-proxy entry shim
```
diff --git a/docs/architecture/meta.json b/docs/architecture/meta.json
index dba5872324be..c5b10b79230a 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/compression/COMPRESSION_ENGINES.md b/docs/compression/COMPRESSION_ENGINES.md
index 22cffe7c42da..4a232a0e5d6c 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/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg
index 99bc29b327e4..4fd6887859f8 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 Providers 1f3a9c2e anthropic Claude Max 20x active 8c2d5b1a codex Codex Pro (team) active f4e0a97b glm GLM Coding Plan active 03bd6e5f 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 80b3cbcdb2dd..d73a1fb25509 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 a25437f78cf9..1c3f0a6cc9cd 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. 341 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 341 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 0182df44a243..fb332a457441 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 → 341 providers — 90+ free — through one endpoint.
+ Every AI tool → 346 providers — 90+ free — through one endpoint.
Claude Code · Codex · Cursor · Cline · Copilot · Antigravity → FREE Claude / GPT / Gemini · auto-fallback
diff --git a/docs/frameworks/MCP-SERVER.md b/docs/frameworks/MCP-SERVER.md
index 67e946528f1a..18877057fd91 100644
--- a/docs/frameworks/MCP-SERVER.md
+++ b/docs/frameworks/MCP-SERVER.md
@@ -347,6 +347,8 @@ per-key path take precedence once it is. stdio has no per-caller identity (see
| `OMNIROUTE_MCP_SCOPES` | (empty) | Comma-separated allowlist of scopes considered "available" by default (used when caller does not provide its own scopes) |
| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | (unset = on) | When set to `0/false/off/no`, disables MCP description compression at registration time |
| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | (unset = on) | Alternate alias for the same toggle as above |
+| `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` | `10000` | Abort budget for internal management reads (health, resilience, combos, quota, usage) |
+| `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS` | `60000` | Abort budget for hops that wait on a provider (`route_request`, `web_search`, `web_fetch`) |
| `MCP_TOOL_DENY` | (unset = no filter) | Comma-separated tool names to drop from `tools/list` (tool-cardinality reduction — see below) |
| `MCP_TOOL_ALLOW` | (unset = no filter) | Comma-separated tool names to keep exclusively (allow-list mode — see below) |
| `DATA_DIR` | `~/.omniroute` | Heartbeat file is written to `${DATA_DIR}/runtime/mcp-heartbeat.json` |
diff --git a/docs/getting-started/AUTO-COMBO-GUIDE.md b/docs/getting-started/AUTO-COMBO-GUIDE.md
index b5797c780e56..fbba5abbe77a 100644
--- a/docs/getting-started/AUTO-COMBO-GUIDE.md
+++ b/docs/getting-started/AUTO-COMBO-GUIDE.md
@@ -189,7 +189,7 @@ Use `auto/smart` when you want the best quality and are okay with occasional exp
### "Can I force a specific provider?"
-Yes! Use a combo with `priority` strategy instead of `auto`. See the [Technical Reference](../routing/AUTO-COMBO.md) for details.
+Yes! Use a combo with `priority` strategy instead of `auto`, then send the combo's **exact name** as the `model` field (e.g. `model: "my-combo"` — not `auto`). See the [Technical Reference](../routing/AUTO-COMBO.md) for details.
### "How is this different from round-robin?"
diff --git a/docs/getting-started/PROVIDERS-GUIDE.md b/docs/getting-started/PROVIDERS-GUIDE.md
index d54b656a8ab7..d6d20906c59e 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
@@ -239,3 +329,7 @@ Go to Providers → click on the provider → click **Disconnect**.
- **[Free Tiers Guide](./FREE-TIERS-GUIDE.md)** — Get free AI with no credit card
- **[Troubleshooting](../guides/TROUBLESHOOTING.md)** — Fix common issues
- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — Full list of 226 providers
+
+## Cursor images
+
+Cursor plan images use `IMAGE_PROVIDERS.cursor` (`cursor-agent-image`). See [CURSOR_IMAGE.md](../providers/CURSOR_IMAGE.md).
diff --git a/docs/getting-started/QUICK-START.md b/docs/getting-started/QUICK-START.md
index d33693443669..bed9fcb71e35 100644
--- a/docs/getting-started/QUICK-START.md
+++ b/docs/getting-started/QUICK-START.md
@@ -26,6 +26,8 @@ npm install -g omniroute
docker run -d --name omniroute -p 20128:20128 diegosouzapw/omniroute:latest
```
+`:latest` is the highest **published** stable SemVer. It does **not** track git `main`. Pin `diegosouzapw/omniroute:X.Y.Z` for GitOps. See [Image Tags / Release Channels](../guides/DOCKER_GUIDE.md#release-channels).
+
### Option C: From Source
```bash
diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md
index 464aa5531931..40487ee6cc12 100644
--- a/docs/guides/DOCKER_GUIDE.md
+++ b/docs/guides/DOCKER_GUIDE.md
@@ -22,6 +22,7 @@ lastUpdated: 2026-06-28
- [Docker Compose with Caddy (HTTPS)](#docker-compose-with-caddy-https-auto-tls)
- [Cloudflare Quick Tunnel](#cloudflare-quick-tunnel)
- [Image Tags](#image-tags)
+- [Availability: default SQLite is single-replica](#availability-default-sqlite-is-single-replica)
- [Important Notes](#important-notes)
---
@@ -341,13 +342,15 @@ For orchestrators (Kubernetes, Nomad, etc.):
| Probe | Prefer | Avoid |
| --- | --- | --- |
-| Liveness | TCP on the main port (`PORT`, default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` as liveness |
+| Liveness | HTTP `GET /livez`, or TCP on the main port (`PORT`, default `20128`) | `/api/monitoring/health` as liveness |
| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead |
| Deep / blackbox | `/api/monitoring/health` | — |
-`/healthz` only reports process lifecycle (`ok` / `starting` / `stopping`). It still
-runs on the same Node event loop as request handling, so CPU-bound catalog or
-compression work can delay it — busy ≠ dead. Full probe guidance:
+`/healthz` reports process lifecycle (`ok` / `starting` / `stopping`). `/livez` is
+process-alive only (200 whenever the handler can run; it does not wait for
+readiness). Both still run on the same Node event loop as request handling, so
+CPU-bound catalog or compression work can delay them — busy ≠ dead. Prefer TCP
+liveness if HTTP probes time out. Full probe guidance:
[Monitoring guide — Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations).
## Docker Compose with Caddy (HTTPS Auto-TLS)
@@ -409,8 +412,8 @@ Endpoint tunnel panels (Cloudflare, Tailscale, ngrok) can be shown or hidden fro
| Image | Tag | Size | Description |
| ------------------------ | -------- | ------ | --------------------- |
-| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release |
-| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Current version |
+| `diegosouzapw/omniroute` | `latest` | ~250MB | Highest **published** stable SemVer (not git `main`) |
+| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Pin this class of tag for GitOps |
Multi-platform manifest: `linux/amd64` + `linux/arm64` native (Apple Silicon, AWS Graviton, Raspberry Pi). Docker selects the matching architecture automatically; pass `--platform linux/amd64` if you need to force AMD64 emulation on ARM hosts.
@@ -421,7 +424,7 @@ OmniRoute publishes separate Docker channels for stable releases, active release
| Channel | Source | Mutability | Recommended use |
| ------------------------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- |
| `:` / `:-web` | Signed/versioned release | Immutable | Production deployments that pin an exact release |
-| `:latest` / `:latest-web` | Highest stable release | Mutable stable pointer | Production deployments that intentionally follow stable releases |
+| `:latest` / `:latest-web` | Highest **published** stable SemVer | Mutable stable pointer | Follows stable releases **after** a SemVer publish job — does **not** track `main` or unreleased `release/v*` commits |
| `:next` / `:next-web` | Current default `release/v*` branch | Mutable pre-release pointer | Testing fixes that have landed on the active release branch but are not yet in a stable release |
| `:main` / `:main-web` | `main` branch | Mutable development pointer | Development and integration testing only |
@@ -465,6 +468,37 @@ docker compose up -d
A release-branch build can never move `latest`; only an eligible stable semantic version may promote the stable pointer. The `next` images retain the release image inspection and blocking CRITICAL-vulnerability gate.
+**`latest` is not a currency guarantee for git.** Merged fixes on `main` or on the active `release/v*` branch are **not** in `:latest` until a stable SemVer image is published and the publish job promotes `:latest` (same digest as that SemVer). If `latest` looks frozen while GitHub already shows the fix, pull `:next` to test the release branch or wait for the SemVer tag.
+
+| You want | Use |
+| --- | --- |
+| GitOps / production that must not drift | Pin `:X.Y.Z` (or the image digest) |
+| Follow published stables and accept a recreate on each release | `:latest` |
+| Test unreleased `release/v*` commits | `:next` (not production) |
+| Test `main` | `:main` (not production) |
+
+## Availability: default SQLite is single-replica
+
+Stock Docker / Kubernetes OmniRoute is **one Node process + one SQLite writer**. High availability is **not supported** on that topology.
+
+| Constraint | Consequence |
+| --- | --- |
+| Single writer | Do **not** run multiple replicas against the same SQLite file. That corrupts the DB. |
+| Recreate / restart / HEALTHCHECK kill | **Full outage** of in-flight SSE, dashboard sessions, and in-memory state. Every connected client drops. |
+| Same event loop as `/healthz` | A busy catalog or compression tick can delay probes; a short timeout then restarts the **only** replica. |
+
+**Probe matrix** (see also [Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations)):
+
+| Probe | Target | Do not use |
+| --- | --- | --- |
+| Liveness | TCP on `PORT` (default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` |
+| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead |
+| Deep / humans | `/api/monitoring/health` | Automated kubelet liveness |
+
+**Upgrades:** expect every session to drop. Drain clients if you can; there is no rolling update on default SQLite. Compose `restart: unless-stopped` plus Docker `HEALTHCHECK` will also replace the only process when the container is Unhealthy — same blast radius.
+
+External Postgres / multi-writer HA is **not** a documented stock path. If you need HA, keep a single replica or run a topology the project has tested and documented separately.
+
## Important Notes
- **SQLite WAL Mode:** `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`. The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40`.
diff --git a/docs/guides/FREE_PROVIDER_RANKINGS.md b/docs/guides/FREE_PROVIDER_RANKINGS.md
index 7eb49f466092..79d2ba6ed804 100644
--- a/docs/guides/FREE_PROVIDER_RANKINGS.md
+++ b/docs/guides/FREE_PROVIDER_RANKINGS.md
@@ -169,7 +169,7 @@ stale data.
The sync runs **on by default**:
- It runs once at server startup and then on a periodic timer
- (`src/lib/arenaEloSync.ts`, wired from `src/server-init.ts`).
+ (`src/lib/arenaEloSync.ts`, wired from `src/instrumentation-node.ts`).
- It is **non-blocking and never fatal** — if the upstream fetch fails, OmniRoute keeps
running and the rankings simply show the last good data (or an empty state).
diff --git a/docs/guides/MANAGEMENT-AUTH.md b/docs/guides/MANAGEMENT-AUTH.md
index 25e0d7ae5966..5e0d8d6eeb3c 100644
--- a/docs/guides/MANAGEMENT-AUTH.md
+++ b/docs/guides/MANAGEMENT-AUTH.md
@@ -1,47 +1,159 @@
---
title: "Management Authentication"
version: 3.8.50
-lastUpdated: 2026-08-05
+lastUpdated: 2026-08-20
---
# Management Authentication
-OmniRoute uses four distinct credential families for management access. This guide
-distinguishes them by purpose, scope, and locality.
+OmniRoute has **four credential families** that can authorize management routes.
+They are not interchangeable. Inference API keys (`sk-…`) do **not** manage the
+server unless they were explicitly granted `manage` or `admin` scope.
-| Credential | Scope | Locality | Use Case |
-|-------------------------|--------------------|---------------|-----------------------------------|
-| Dashboard JWT session | Full management | Localhost | Web dashboard login |
-| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
-| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
-| Manage-scope API key | `manage` scope | External | Management API calls |
+Canonical implementation: `src/lib/api/requireManagementAuth.ts`.
-## Dashboard JWT Session
+| Credential | Typical form | Created where | Intended use | Management capability |
+|---|---|---|---|---|
+| Dashboard 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` |
-Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
-Valid for the session duration. Cannot be used from external hosts.
+`oma_` credentials are management/CLI credentials. They are **not** inference API keys.
-## CLI Machine-ID Token
+If login/API-key auth is disabled for the server, some management routes may
+accept unauthenticated calls. Local-only and always-protected routes still apply
+their own rules. Presenting one of these credentials is therefore not universally
+mandatory, and possessing one is not universally sufficient without the required
+scope and route locality.
-Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
-Used by the CLI for all management operations. Tied to the machine identity.
+Related: [Remote Mode](./REMOTE-MODE.md) (how `oma_live_…` is minted for a remote CLI).
-## Scoped `oma_` Access Token
+---
+
+## Scope matrices
+
+These two scope vocabularies are **different**. Do not mix them.
+
+### Access Token scopes (`oma_live_…`)
+
+| Scope | Typical operations |
+|---|---|
+| `read` | List/status GETs that the token is allowed to see |
+| `write` | Mutations (create/update/delete) below admin |
+| `admin` | Full remote CLI / connect token (password bootstrap defaults here) |
+
+A token with `read` cannot call a `write` route. Runtime message shape:
+`Access token scope '' is insufficient; '' required.`
+
+### API-key management scopes
+
+| Scope | Meaning |
+|---|---|
+| (none) | Inference only. Management routes return 403. |
+| `manage` | Management API (same gate as `requireManagementAuth` API-key branch) |
+| `admin` | Also satisfies `hasManageScope` (treated as management-capable) |
+
+Enable `manage` on the key in the API Keys / API Manager UI. Do not reuse a
+chat client key for automation unless you deliberately granted that scope.
+
+---
+
+## How to create and revoke
+
+### Dashboard 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.
+
+### CLI machine-id token
-Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
-Format: `oma_`. Used for programmatic access from external systems.
+1. Run `omniroute` on the **same host** as the server (loopback).
+2. The CLI bootstraps a machine-id token under `~/.omniroute/` (chmod 600).
+3. This does **not** work from another machine. Use an Access Token for remote CLI.
-## Manage-Scope API Key
+### Scoped Access Token (`oma_live_…`)
-Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
-Used for management API calls from external hosts.
+1. Dashboard: **Settings → Access Tokens** → create (name + scope). **The secret is shown once.**
+2. Or CLI: `omniroute connect ` (password → token). See [Remote Mode](./REMOTE-MODE.md).
+3. Header: `Authorization: Bearer oma_live_…`
+4. Revoke from the same Access Tokens page (or delete the CLI context).
+5. Server stores only a hash. Treat the plaintext like a password.
-## Header Examples
+### Manage-scoped API key
+1. Dashboard: **API Manager / API Keys** → create or edit a key → enable `manage` (or `admin`).
+2. Header: `Authorization: Bearer sk-…` (the key's actual prefix).
+3. Revoke or strip `manage` in the same UI.
+4. Least privilege for automation that is not the CLI: prefer a `read` Access Token for GET-only jobs; use `manage` on an API key only when the caller must also speak `/v1` and management.
+
+---
+
+## Header format
+
+```http
+Authorization: Bearer oma_live_
+Authorization: Bearer sk-
+Cookie: auth_token=
```
-Authorization: Bearer oma_abc123def456
-Authorization: Bearer
-Cookie: omniroute_session=
+
+Do not put management credentials in the URL path or query string. Management
+auth is header/cookie only.
+
+---
+
+## Copy-paste examples
+
+Read-only (list providers). Use a `read` Access Token:
+
+```bash
+curl -sS "$OMNIROUTE_URL/api/providers" \
+ -H "Authorization: Bearer oma_live_"
```
-See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
+Modifying (create a provider connection). Use `write`/`admin` Access Token or a
+manage-scoped API key:
+
+```bash
+curl -sS -X POST "$OMNIROUTE_URL/api/providers" \
+ -H "Authorization: Bearer oma_live_" \
+ -H "Content-Type: application/json" \
+ -d '{"provider":"openai","apiKey":""}'
+```
+
+Inference (not management). Ordinary API key, no `manage` required:
+
+```bash
+curl -sS "$OMNIROUTE_URL/v1/models" \
+ -H "Authorization: Bearer sk-"
+```
+
+---
+
+## Current runtime errors (do not echo secrets)
+
+| Situation | Typical status | Message (sanitized) |
+|---|---|---|
+| No credential | 401 | `Authentication required` |
+| Invalid/expired `oma_live_…` | 401 | `Invalid or expired access token` |
+| Valid API key without `manage`/`admin` | 403 | `API key lacks 'manage' scope. Enable it in the API Keys dashboard.` |
+| Invalid ordinary API key on a management route | 403 | `Invalid management token` |
+| Access Token scope too low | 403 | `Access token scope '' is insufficient; '' required.` |
+
+"Invalid management token" means the bearer was **not** accepted as a management
+credential. It does **not** tell you which family to mint. Use the table above:
+inference keys need `manage` scope; remote CLI needs `oma_live_…`; the dashboard
+uses the session cookie.
+
+---
+
+## Recommended least-privilege choice
+
+| Caller | Use |
+|---|---|
+| Browser | Dashboard session |
+| CLI on the server host | Machine token |
+| CLI on a laptop talking to a remote server | `oma_live_…` from `omniroute connect` |
+| CI / scripts (management only) | `oma_live_…` with the smallest scope that works |
+| CI that must call both `/v1` and `/api` | API key with `manage` **or** two credentials |
diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt
index d5a97ec33a48..53dfd1c0d6c7 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 6116eb395e62..cf0018a415ef 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 6116eb395e62..cf0018a415ef 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 18ab0fc53732..2749b7b10813 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 08f8297c101f..76a283564b7a 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 531f324c3ae0..5bcb53ef1048 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 dfefdf04bf2c..17cb4a70bf60 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 cd36f88e0726..0441be519d14 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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/README.md b/docs/i18n/fa/README.md
index beab2b5e95d5..15cb5ef09aae 100644
--- a/docs/i18n/fa/README.md
+++ b/docs/i18n/fa/README.md
@@ -1,14 +1,14 @@
-# 🚀 OmniRoute — The Free AI Gateway (فارسی)
+# 🚀 OmniRoute — درگاه رایگان هوش مصنوعی (فارسی)
-🌐 **Languages:** 🇺🇸 [English](../../../README.md) · 🇸🇦 [ar](../ar/README.md) · 🇧🇬 [bg](../bg/README.md) · 🇧🇩 [bn](../bn/README.md) · 🇨🇿 [cs](../cs/README.md) · 🇩🇰 [da](../da/README.md) · 🇩🇪 [de](../de/README.md) · 🇪🇸 [es](../es/README.md) · 🇮🇷 [fa](../fa/README.md) · 🇫🇮 [fi](../fi/README.md) · 🇫🇷 [fr](../fr/README.md) · 🇮🇳 [gu](../gu/README.md) · 🇮🇱 [he](../he/README.md) · 🇮🇳 [hi](../hi/README.md) · 🇭🇺 [hu](../hu/README.md) · 🇮🇩 [id](../id/README.md) · 🇮🇹 [it](../it/README.md) · 🇯🇵 [ja](../ja/README.md) · 🇰🇷 [ko](../ko/README.md) · 🇮🇳 [mr](../mr/README.md) · 🇲🇾 [ms](../ms/README.md) · 🇳🇱 [nl](../nl/README.md) · 🇳🇴 [no](../no/README.md) · 🇵🇭 [phi](../phi/README.md) · 🇵🇱 [pl](../pl/README.md) · 🇵🇹 [pt](../pt/README.md) · 🇧🇷 [pt-BR](../pt-BR/README.md) · 🇷🇴 [ro](../ro/README.md) · 🇷🇺 [ru](../ru/README.md) · 🇸🇰 [sk](../sk/README.md) · 🇸🇪 [sv](../sv/README.md) · 🇰🇪 [sw](../sw/README.md) · 🇮🇳 [ta](../ta/README.md) · 🇮🇳 [te](../te/README.md) · 🇹🇭 [th](../th/README.md) · 🇹🇷 [tr](../tr/README.md) · 🇺🇦 [uk-UA](../uk-UA/README.md) · 🇵🇰 [ur](../ur/README.md) · 🇻🇳 [vi](../vi/README.md) · 🇨🇳 [zh-CN](../zh-CN/README.md)
+🌐 **زبانها:** 🇺🇸 [English](../../../README.md) · 🇸🇦 [ar](../ar/README.md) · 🇧🇬 [bg](../bg/README.md) · 🇧🇩 [bn](../bn/README.md) · 🇨🇿 [cs](../cs/README.md) · 🇩🇰 [da](../da/README.md) · 🇩🇪 [de](../de/README.md) · 🇪🇸 [es](../es/README.md) · 🇮🇷 [fa](../fa/README.md) · 🇫🇮 [fi](../fi/README.md) · 🇫🇷 [fr](../fr/README.md) · 🇮🇳 [gu](../gu/README.md) · 🇮🇱 [he](../he/README.md) · 🇮🇳 [hi](../hi/README.md) · 🇭🇺 [hu](../hu/README.md) · 🇮🇩 [id](../id/README.md) · 🇮🇹 [it](../it/README.md) · 🇯🇵 [ja](../ja/README.md) · 🇰🇷 [ko](../ko/README.md) · 🇮🇳 [mr](../mr/README.md) · 🇲🇾 [ms](../ms/README.md) · 🇳🇱 [nl](../nl/README.md) · 🇳🇴 [no](../no/README.md) · 🇵🇭 [phi](../phi/README.md) · 🇵🇱 [pl](../pl/README.md) · 🇵🇹 [pt](../pt/README.md) · 🇧🇷 [pt-BR](../pt-BR/README.md) · 🇷🇴 [ro](../ro/README.md) · 🇷🇺 [ru](../ru/README.md) · 🇸🇰 [sk](../sk/README.md) · 🇸🇪 [sv](../sv/README.md) · 🇰🇪 [sw](../sw/README.md) · 🇮🇳 [ta](../ta/README.md) · 🇮🇳 [te](../te/README.md) · 🇹🇭 [th](../th/README.md) · 🇹🇷 [tr](../tr/README.md) · 🇺🇦 [uk-UA](../uk-UA/README.md) · 🇵🇰 [ur](../ur/README.md) · 🇻🇳 [vi](../vi/README.md) · 🇨🇳 [zh-CN](../zh-CN/README.md)
---
-### Keep coding through provider limits. Smart routing to free-access and low-cost AI models with automatic fallback.
+### کدنویسی را از طریق محدودیت های ارائه دهنده ادامه دهید. مسیریابی هوشمند به مدلهای هوش مصنوعی با دسترسی رایگان و کمهزینه با بازگشت خودکار.
-_Your universal API proxy — one endpoint, 329 provider catalog entries, resilient fallback subject to upstream availability. Includes **MCP Server (107 tools, 32 scopes)**, **A2A Protocol**, **Memory/Skills Systems** & **Electron Desktop App**._
+_پراکسی جهانی API شما - یک نقطه پایانی، 329 ورودی کاتالوگ ارائهدهنده، بازگشت انعطافپذیر به شرط در دسترس بودن بالادست. شامل **سرور MCP (107 ابزار، 32 دامنه)**، **پروتکل A2A**، **سیستم های حافظه/مهارت** و **برنامه دسکتاپ الکترونیک**._
-**Chat Completions • Embeddings • Image Generation • Video • Music • Audio • Reranking • **Web Search** • MCP Server • A2A Protocol • 100% TypeScript**
+** تکمیل چت • جاسازی ها • تولید تصویر • ویدئو • موسیقی • صدا • رتبه بندی مجدد • **جستجوی وب** • سرور MCP • پروتکل A2A • 100% TypeScript**
---
@@ -17,12 +17,12 @@ _Your universal API proxy — one endpoint, 329 provider catalog entries, resili
[](https://www.npmjs.com/package/omniroute)
[](https://hub.docker.com/r/diegosouzapw/omniroute)
-
-
+
+
-
+

-
+
[](https://github.com/diegosouzapw/OmniRoute/stargazers)
[](https://github.com/diegosouzapw/OmniRoute/issues)
@@ -45,11 +45,11 @@ _Your universal API proxy — one endpoint, 329 provider catalog entries, resili
-🌐 **Available in:** 🇺🇸 [English](README.md) | 🇧🇷 [Português (Brasil)](docs/i18n/pt-BR/README.md) | 🇪🇸 [Español](docs/i18n/es/README.md) | 🇫🇷 [Français](docs/i18n/fr/README.md) | 🇮🇹 [Italiano](docs/i18n/it/README.md) | 🇷🇺 [Русский](docs/i18n/ru/README.md) | 🇨🇳 [中文 (简体)](docs/i18n/zh-CN/README.md) | 🇩🇪 [Deutsch](docs/i18n/de/README.md) | 🇮🇳 [हिन्दी](docs/i18n/in/README.md) | 🇹🇭 [ไทย](docs/i18n/th/README.md) | 🇺🇦 [Українська](docs/i18n/uk-UA/README.md) | 🇸🇦 [العربية](docs/i18n/ar/README.md) | 🇯🇵 [日本語](docs/i18n/ja/README.md) | 🇻🇳 [Tiếng Việt](docs/i18n/vi/README.md) | 🇧🇬 [Български](docs/i18n/bg/README.md) | 🇩🇰 [Dansk](docs/i18n/da/README.md) | 🇫🇮 [Suomi](docs/i18n/fi/README.md) | 🇮🇱 [עברית](docs/i18n/he/README.md) | 🇭🇺 [Magyar](docs/i18n/hu/README.md) | 🇮🇩 [Bahasa Indonesia](docs/i18n/id/README.md) | 🇰🇷 [한국어](docs/i18n/ko/README.md) | 🇲🇾 [Bahasa Melayu](docs/i18n/ms/README.md) | 🇳🇱 [Nederlands](docs/i18n/nl/README.md) | 🇳🇴 [Norsk](docs/i18n/no/README.md) | 🇵🇹 [Português (Portugal)](docs/i18n/pt/README.md) | 🇷🇴 [Română](docs/i18n/ro/README.md) | 🇵🇱 [Polski](docs/i18n/pl/README.md) | 🇸🇰 [Slovenčina](docs/i18n/sk/README.md) | 🇸🇪 [Svenska](docs/i18n/sv/README.md) | 🇵🇭 [Filipino](docs/i18n/phi/README.md) | 🇨🇿 [Čeština](docs/i18n/cs/README.md)
+🌐 **موجود در:** 🇺🇸 [انگلیسی](README.md) | 🇧🇷 [پرتغال (برزیل)](docs/i18n/pt-BR/README.md) | 🇪🇸 [اسپانیول](docs/i18n/es/README.md) | 🇫🇷 [Français](docs/i18n/fr/README.md) | 🇮🇹 [ایتالیانو](docs/i18n/it/README.md) | 🇷🇺 [Русский](docs/i18n/ru/README.md) | 🇨🇳 [中文 (简体)](docs/i18n/zh-CN/README.md) | 🇩🇪 [دویچ](docs/i18n/de/README.md) | 🇮🇳 [हिन्दी](docs/i18n/in/README.md) | 🇹🇭 [ไทย](docs/i18n/th/README.md) | 🇺🇦 [Українська](docs/i18n/uk-UA/README.md) | 🇸🇦 [العربیة](docs/i18n/ar/README.md) | 🇯🇵 [日本語](docs/i18n/ja/README.md) | 🇻🇳 [Tiếng Việt](docs/i18n/vi/README.md) | 🇧🇬 [Български](docs/i18n/bg/README.md) | 🇩🇰 [دانسک](docs/i18n/da/README.md) | 🇫🇮 [Suomi](docs/i18n/fi/README.md) | 🇮🇱 [עברית](docs/i18n/he/README.md) | 🇭🇺 [مگیار](docs/i18n/hu/README.md) | 🇮🇩 [Bahasa Indonesia](docs/i18n/id/README.md) | 🇰🇷 [한국어](docs/i18n/ko/README.md) | 🇲🇾 [Bahasa Melayu](docs/i18n/ms/README.md) | 🇳🇱 [هلند](docs/i18n/nl/README.md) | 🇳🇴 [نورسک](docs/i18n/no/README.md) | 🇵🇹 [پرتغال (پرتغال)](docs/i18n/pt/README.md) | 🇷🇴 [Română](docs/i18n/ro/README.md) | 🇵🇱 [Polski](docs/i18n/pl/README.md) | 🇸🇰 [Slovenčina](docs/i18n/sk/README.md) | 🇸🇪 [Svenska](docs/i18n/sv/README.md) | 🇵🇭 [فیلیپینی](docs/i18n/phi/README.md) | 🇨🇿 [Čeština](docs/i18n/cs/README.md)
---
-## 🖼️ Main Dashboard
+## 🖼️ داشبورد اصلی
@@ -57,30 +57,30 @@ _Your universal API proxy — one endpoint, 329 provider catalog entries, resili
---
-## 📸 Dashboard Preview
+## 📸 پیش نمایش داشبورد
Click to see dashboard screenshots
-| Page | Screenshot |
+| صفحه | اسکرین شات |
| -------------- | ------------------------------------------------- |
-| **Providers** |  |
-| **Combos** |  |
-| **Analytics** |  |
-| **Health** |  |
-| **Translator** |  |
-| **Settings** |  |
-| **CLI Tools** |  |
-| **Usage Logs** |  |
-| **Endpoints** |  |
+| **ارائه دهندگان** |  |
+| **ترکیب** |  |
+| **تحلیل** |  |
+| **سلامت** |  |
+| **مترجم** |  |
+| **تنظیمات** |  |
+| **ابزار CLI** |  |
+| ** سیاهههای استفاده ** |  |
+| **نقاط پایانی** |  |
---
-### 🤖 Free AI Provider for your favorite coding agents
+### 🤖 ارائه دهنده رایگان هوش مصنوعی برای عوامل برنامه نویسی مورد علاقه شما
-_Connect any AI-powered IDE or CLI tool through OmniRoute — free-access AI gateway; provider limits and terms apply._
+_هر ابزار IDE یا CLI مجهز به هوش مصنوعی را از طریق OmniRoute - دروازه هوش مصنوعی با دسترسی آزاد وصل کنید. محدودیت ها و شرایط ارائه دهنده اعمال می شود._
@@ -138,14 +138,14 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free-access AI gat
- Claude Code
+ کد Claude
⭐ 67.3K
- Kilo Code
+ کد کیلو
⭐ 15.5K
@@ -156,46 +156,46 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free-access AI gat
---
-## 🤔 Why OmniRoute?
+## 🤔 چرا OmniRoute؟
-**Stop wasting money and hitting limits:**
+**از هدر دادن پول و رسیدن به محدودیت ها جلوگیری کنید:**
-- Subscription quota expires unused every month
-- Rate limits stop you mid-coding
-- Expensive APIs ($20-50/month per provider)
-- Manual switching between providers
+- سهمیه اشتراک بدون استفاده هر ماه منقضی می شود
+- محدودیت های نرخ شما را در میانه کدنویسی متوقف می کند
+- API های گران قیمت (20-50 دلار در ماه برای هر ارائه دهنده)
+- تعویض دستی بین ارائه دهندگان
-**OmniRoute solves this:**
+**OmniRoute این مشکل را حل می کند:**
-- ✅ **Maximize subscriptions** - Track quota, use every bit before reset
-- ✅ **Auto fallback** - Subscription → API Key → Cheap → Free; availability depends on eligible upstream routes
-- ✅ **Multi-account** - Round-robin between accounts per provider
+- ✅ ** اشتراک ها را به حداکثر برسانید ** - سهمیه را پیگیری کنید، از هر بیت قبل از تنظیم مجدد استفاده کنید
+- ✅ ** بازگشت خودکار ** - اشتراک → کلید API → ارزان → رایگان. در دسترس بودن بستگی به مسیرهای واجد شرایط بالادست دارد
+- ✅ **چند حساب ** - دور برگشت بین حساب ها در هر ارائه دهنده
---
-## 📧 Support
+## 📧 پشتیبانی
-> 💬 **Join our community!** [WhatsApp Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Get help, share tips, and stay updated.
+> 💬 **به انجمن ما بپیوندید!** [گروه واتساپ](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) - راهنمایی دریافت کنید، نکات را به اشتراک بگذارید و به روز بمانید.
-- **Website**: [omniroute.online](https://omniroute.online)
+- **وب سایت**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
-- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
-- **WhatsApp**: [Community Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
-- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md), open a PR, or pick a `good first issue`
+- **مشکلات**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
+- **WhatsApp**: [گروه انجمن](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
+- **مشارکت**: به [CONTRIBUTING.md](CONTRIBUTING.md) مراجعه کنید، یک PR باز کنید، یا یک `good first issue` انتخاب کنید
-### 🐛 Reporting a Bug?
+### 🐛 یک اشکال را گزارش می کنید؟
-When opening an issue, please run the system-info command and attach the generated file:
+هنگام باز کردن یک مشکل، لطفاً دستور system-info را اجرا کنید و فایل تولید شده را پیوست کنید:
```bash
npm run system-info
```
-This generates a `system-info.txt` with your Node.js version, OmniRoute version, OS details, installed CLI tools (qoder, gemini, claude, codex, antigravity, droid, etc.), Docker/PM2 status, and system packages — everything we need to reproduce your issue quickly. Attach the file directly to your GitHub issue.
+این فرمان فایل `system-info.txt` را شامل نسخه Node.js و OmniRoute، جزئیات سیستمعامل، ابزارهای CLI نصبشده (qoder، gemini، claude، codex، antigravity، droid و غیره)، وضعیت Docker/PM2 و بستههای سیستم تولید میکند؛ هرآنچه برای بازتولید سریع مشکل لازم است. فایل را مستقیماً به گزارش مشکل GitHub پیوست کنید.
---
-## 🔄 How It Works
+## 🔄 چگونه کار می کند
```
┌─────────────┐
@@ -224,467 +224,467 @@ Result: broader fallback coverage and cost control; availability is not guarante
---
-## 🎯 What OmniRoute Solves — 30 Real Pain Points & Use Cases
+## 🎯 آنچه OmniRoute حل می کند - 30 نقطه درد واقعی و موارد استفاده
-> **Every developer using AI tools faces these problems daily.** OmniRoute was built to solve them all — from cost overruns to regional blocks, from broken OAuth flows to protocol operations and enterprise observability.
+> **هر برنامهنویسی که از ابزارهای هوش مصنوعی استفاده میکند، روزانه با این مشکلات روبرو میشود. ** OmniRoute برای حل همه آنها ساخته شده است - از مازاد هزینه تا بلوکهای منطقهای، از جریانهای شکسته OAuth تا عملیات پروتکل و قابلیت مشاهده سازمانی.
-💸 1. "I pay for an expensive subscription but still get interrupted by limits"
+💸 ۱. «برای اشتراک گرانقیمت پول میدهم، اما محدودیتها همچنان کارم را قطع میکنند»
-Developers pay $20–200/month for Claude Pro, Codex Pro, or GitHub Copilot. Even paying, quota has a ceiling — 5h of usage, weekly limits, or per-minute rate limits. Mid-coding session, the provider stops responding and the developer loses flow and productivity.
+توسعهدهندگان 20 تا 200 دلار در ماه برای Claude Pro، Codex Pro، یا GitHub Copilot میپردازند. حتی با پرداخت، سهمیه سقفی دارد - 5 ساعت استفاده، محدودیت های هفتگی یا محدودیت نرخ در دقیقه. در اواسط جلسه کدنویسی، ارائه دهنده پاسخ نمی دهد و توسعه دهنده جریان و بهره وری را از دست می دهد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Smart 4-Tier Fallback** — If subscription quota runs out, automatically redirects to API Key → Cheap → Free with zero manual intervention
-- **Provider Limits Tracking** — Cached quota snapshots refresh on a server-side schedule (default `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70`) with manual refresh available in the UI
-- **Multi-Account Support** — Multiple accounts per provider with auto round-robin — when one runs out, switches to the next
-- **Custom Combos** — Customizable fallback chains with 13 balancing strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, strict-random, auto, lkgp, context-optimized, **context-relay**)
-- **Structured Combo Builder** — Build combos step-by-step with explicit provider + model + account selection, including repeated providers and fixed-account targets
-- **Quota-Aware P2C** — Power-of-two account selection now factors quota headroom, backoff, recent errors, and consecutive use
-- **Codex Business Quotas** — Business/Team workspace quota monitoring directly in the dashboard
+- ** بازگشت هوشمند 4-سطحی ** - اگر سهمیه اشتراک تمام شود، به طور خودکار به کلید API هدایت می شود → ارزان → رایگان با دخالت دستی صفر
+- **ردیابی محدودیت های ارائه دهنده** - عکس های لحظه ای سهمیه ذخیره شده در حافظه پنهان در یک برنامه زمانی سمت سرور (پیش فرض `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70`) با بازخوانی دستی موجود در رابط کاربری بازخوانی می شوند.
+- **پشتیبانی چند حساب** - چندین حساب در هر ارائه دهنده با چرخش خودکار - وقتی یکی تمام شد، به حساب بعدی تغییر می کند
+- **ترکیب های سفارشی** - زنجیره های بازگشتی قابل تنظیم با 13 استراتژی متعادل کننده (اولویت، وزن، پر کردن، دور روبین، P2C، تصادفی، کم استفاده، بهینه سازی هزینه، تصادفی دقیق، خودکار، lkgp، بهینه سازی زمینه، **رله زمینه**)
+- **سازگار ترکیبی ساختاریافته** - ساخت ترکیبی گام به گام با ارائه دهنده صریح + مدل + انتخاب حساب، از جمله ارائه دهندگان مکرر و اهداف حساب ثابت
+- **Quota-Aware P2C** - انتخاب اکانت قدرت از دو در حال حاضر سهمیه حجم، عقب نشینی، خطاهای اخیر و استفاده متوالی را فاکتور می کند.
+- ** Codex سهمیه های تجاری ** - نظارت بر سهمیه فضای کاری تجاری/تیم به طور مستقیم در داشبورد
-🔌 2. "I need to use multiple providers but each has a different API"
+🔌 ۲. «به چند ارائهدهنده نیاز دارم، اما هرکدام API متفاوتی دارند»
-OpenAI uses one format, Claude (Anthropic) uses another, Gemini yet another. If a dev wants to test models from different providers or fallback between them, they need to reconfigure SDKs, change endpoints, deal with incompatible formats. Custom providers (FriendLI, NIM) have non-standard model endpoints.
+OpenAI از یک قالب استفاده می کند، Claude (انتروپیک) از فرمت دیگری، جمینی از فرمت دیگری استفاده می کند. اگر یک برنامهنویس بخواهد مدلهایی را از ارائهدهندگان مختلف آزمایش کند یا بین آنها بازگشتی ایجاد کند، باید SDKها را دوباره پیکربندی کند، نقاط پایانی را تغییر دهد، با فرمتهای ناسازگار برخورد کند. ارائه دهندگان سفارشی (FriendLI، NIM) دارای نقاط پایانی مدل غیر استاندارد هستند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Unified Endpoint** — A single `http://localhost:20128/v1` serves as proxy for all 329 provider catalog entries
-- **Format Translation** — Automatic and transparent: OpenAI ↔ Claude ↔ Gemini ↔ Responses API
-- **Response Sanitization** — Strips non-standard fields (`x_groq`, `usage_breakdown`, `service_tier`) that break OpenAI SDK v1.83+
-- **Role Normalization** — Converts `developer` → `system` for non-OpenAI providers; `system` → `user` for GLM/ERNIE
-- **Think Tag Extraction** — Extracts `` blocks from models like DeepSeek R1 into standardized `reasoning_content`
-- **Structured Output for Gemini** — `json_schema` → `responseMimeType`/`responseSchema` automatic conversion
-- **`stream` defaults to `false`** — Aligns with OpenAI spec, avoiding unexpected SSE in Python/Rust/Go SDKs
+- **نقطه پایانی یکپارچه** - یک `http://localhost:20128/v1` به عنوان پروکسی برای تمام 329 ورودی کاتالوگ ارائه دهنده عمل می کند
+- **ترجمه فرمت** — خودکار و شفاف: OpenAI ↔ Claude ↔ Gemini ↔ پاسخ ها API
+- **عفونیسازی پاسخ** - فیلدهای غیر استاندارد (`x_groq`، `usage_breakdown`، `service_tier`) را که OpenAI SDK v1.83+ را میشکنند، حذف میکند.
+- ** عادی سازی نقش ** - تبدیل `developer` → `system` برای ارائه دهندگان غیر OpenAI. `system` → `user` برای GLM/ERNIE
+- **Think Tag Extraction** - بلوک های `` را از مدل هایی مانند DeepSeek R1 به استاندارد `reasoning_content` استخراج می کند
+- **خروجی ساختاریافته برای Gemini** — تبدیل خودکار `json_schema` → `responseMimeType`/`responseSchema`
+- **`stream` به طور پیش فرض روی `false`** تنظیم می شود - با مشخصات OpenAI همسو می شود، از SSE غیرمنتظره در SDK های Python/Rust/Go اجتناب می کند
-🌐 3. "My AI provider blocks my region/country"
+🌐 ۳. «ارائهدهندهٔ هوش مصنوعی، منطقه یا کشور من را مسدود میکند»
-Providers like OpenAI/Codex block access from certain geographic regions. Users get errors like `unsupported_country_region_territory` during OAuth and API connections. This is especially frustrating for developers from developing countries.
+ارائه دهندگانی مانند OpenAI/Codex دسترسی از مناطق جغرافیایی خاص را مسدود می کنند. کاربران در طول اتصالات OAuth و API خطاهایی مانند `unsupported_country_region_territory` دریافت می کنند. این امر به ویژه برای توسعه دهندگان کشورهای در حال توسعه ناامید کننده است.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **3-Level Proxy Config** — Configurable proxy at 3 levels: global (all traffic), per-provider (one provider only), and per-connection/key
-- **Color-Coded Proxy Badges** — Visual indicators: 🟢 global proxy, 🟡 provider proxy, 🔵 connection proxy, always showing the IP
-- **OAuth Token Exchange Through Proxy** — OAuth flow also goes through the proxy, solving `unsupported_country_region_territory`
-- **Connection Tests via Proxy** — Connection tests use the configured proxy (no more direct bypass)
-- **SOCKS5 Support** — Full SOCKS5 proxy support for outbound routing
-- **TLS Fingerprint Spoofing** — Browser-like TLS fingerprint via `wreq-js` to bypass bot detection
-- **🔏 CLI Fingerprint Matching** — Reorders headers and body fields to match native CLI binary signatures, drastically reducing account flagging risk. The proxy IP is preserved — you get both stealth **and** IP masking simultaneously
+- ** پیکربندی پروکسی 3 سطح ** - پروکسی قابل تنظیم در 3 سطح: جهانی (تمام ترافیک)، هر ارائه دهنده (فقط یک ارائه دهنده) و هر اتصال/کلید
+- **نشانهای پروکسی با کد رنگی** - نشانگرهای تصویری: پروکسی جهانی، پروکسی ارائه دهنده، 🔵 پروکسی اتصال، همیشه IP را نشان می دهد
+- ** تبادل رمز OAuth از طریق پروکسی** — جریان OAuth نیز از طریق پروکسی می رود و `unsupported_country_region_territory` را حل می کند
+- **تست های اتصال از طریق پروکسی** - تست های اتصال از پروکسی پیکربندی شده استفاده می کنند (دیگر دور زدن مستقیم وجود ندارد)
+- **پشتیبانی SOCKS5** - پشتیبانی کامل از پروکسی SOCKS5 برای مسیریابی خروجی
+- **تقلب اثر انگشت TLS** - اثر انگشت TLS مرورگر مانند از طریق `wreq-js` برای دور زدن تشخیص ربات
+- ** IP پروکسی حفظ می شود - شما هر دو پنهان **و** IP را به طور همزمان دریافت می کنید
-🆓 4. "I want to use AI for coding but I have no money"
+🆓 ۴. «میخواهم برای برنامهنویسی از هوش مصنوعی استفاده کنم، اما بودجهای ندارم»
-Not everyone can pay $20–200/month for AI subscriptions. Students, devs from emerging countries, hobbyists, and freelancers need access to quality models at zero cost.
+همه نمی توانند 20 تا 200 دلار در ماه برای اشتراک هوش مصنوعی بپردازند. دانشآموزان، توسعهدهندگان کشورهای نوظهور، علاقهمندان و مشاغل آزاد نیاز به دسترسی به مدلهای باکیفیت با هزینه صفر دارند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Ollama Cloud** — Cloud-hosted Ollama models at `api.ollama.com` with free "Light usage" tier; use `ollamacloud/` prefix
-- **Free-Only Combos** — Chain `if/kimi-k2-thinking → qw/qwen3-coder-plus` can use currently listed $0 access; limits and availability apply
-- **NVIDIA NIM Free Access** — ~40 RPM free access as currently listed; provider terms and model availability apply at build.nvidia.com (transitioning from credits to pure rate limits)
-- **Cost Optimized Strategy** — Routing strategy that automatically chooses the cheapest available provider
+- **Ollama Cloud** - مدل های Ollama میزبان ابر در `api.ollama.com` با سطح رایگان "استفاده از نور". از پیشوند `ollamacloud/` استفاده کنید
+- **ترکیبهای فقط رایگان** - زنجیره `if/kimi-k2-thinking → qw/qwen3-coder-plus` میتواند از دسترسی $0 فهرستشده فعلی استفاده کند. محدودیت ها و در دسترس بودن اعمال می شود
+- **NVIDIA NIM دسترسی رایگان ** — دسترسی آزاد ~40 RPM همانطور که در حال حاضر لیست شده است. شرایط ارائه دهنده و در دسترس بودن مدل در build.nvidia.com اعمال می شود (انتقال از اعتبار به محدودیت نرخ خالص)
+- **استراتژی بهینه سازی هزینه** - استراتژی مسیریابی که به طور خودکار ارزان ترین ارائه دهنده موجود را انتخاب می کند
-🔒 5. "I need to protect my AI gateway from unauthorized access"
+🔒 ۵. «باید درگاه هوش مصنوعیام را در برابر دسترسی غیرمجاز محافظت کنم»
-When exposing an AI gateway to the network (LAN, VPS, Docker), anyone with the address can consume the developer's tokens/quota. Without protection, APIs are vulnerable to misuse, prompt injection, and abuse.
+هنگامی که یک دروازه هوش مصنوعی را در معرض شبکه قرار می دهید (LAN، VPS، Docker)، هر کسی که آدرس را داشته باشد می تواند توکن ها/سهمیه توسعه دهنده را مصرف کند. بدون محافظت، APIها در برابر سوء استفاده، تزریق سریع و سوء استفاده آسیب پذیر هستند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **API Key Management** — Generation, rotation, and scoping per provider with a dedicated `/dashboard/api-manager` page
-- **Model-Level Permissions** — Restrict API keys to specific models (`openai/*`, wildcard patterns), with Allow All/Restrict toggle
-- **API Endpoint Protection** — Require a key for `/v1/models` and block specific providers from the listing
-- **Auth Guard + CSRF Protection** — All dashboard routes protected with `withAuth` middleware + CSRF tokens
-- **Rate Limiter** — Per-IP rate limiting with configurable windows
-- **IP Filtering** — Allowlist/blocklist for access control
-- **Prompt Injection Guard** — Sanitization against malicious prompt patterns
-- **AES-256-GCM Encryption** — Credentials encrypted at rest
+- **API مدیریت کلید** — تولید، چرخش و محدوده برای هر ارائه دهنده با صفحه اختصاصی `/dashboard/api-manager`
+- **مجوزهای سطح مدل** - کلیدهای API را به مدلهای خاص محدود کنید (`openai/*`، الگوهای عام)، با تغییر حالت Allow All/Restrict
+- **API حفاظت نقطه پایانی** - نیاز به کلید برای `/v1/models` و مسدود کردن ارائه دهندگان خاص از فهرست
+- **Auth Guard + CSRF Protection** - همه مسیرهای داشبورد با میان افزار `withAuth` + توکن های CSRF محافظت می شوند
+- **Rate Limiter** - محدود کردن نرخ به ازای IP با پنجره های قابل تنظیم
+- ** فیلتر IP ** - لیست مجاز / لیست مسدود برای کنترل دسترسی
+- **محافظ تزریق سریع** - ضدعفونی کردن در برابر الگوهای سریع مخرب
+- ** رمزگذاری AES-256-GCM ** - اعتبارنامه ها در حالت استراحت رمزگذاری شده اند
-🛑 6. "My provider went down and I lost my coding flow"
+🛑 ۶. «ارائهدهنده از دسترس خارج شد و جریان برنامهنویسیام را از دست دادم»
-AI providers can become unstable, return 5xx errors, or hit temporary rate limits. If a dev depends on a single provider, they're interrupted. Without circuit breakers, repeated retries can crash the application.
+ارائهدهندگان هوش مصنوعی میتوانند ناپایدار شوند، خطاهای 5xx را برگردانند یا به محدودیتهای نرخ موقت برسند. اگر یک توسعه دهنده به یک ارائه دهنده وابسته باشد، آنها قطع می شوند. بدون قطع کننده مدار، تلاش های مجدد مکرر می تواند برنامه را خراب کند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Request Queue & Pacing** — Per-connection request buckets smooth bursts before they hit upstream rate caps
-- **Connection Cooldown** — A single connection cools down after retryable failures with optional upstream `Retry-After` hints and exponential backoff
-- **Provider Circuit Breaker** — The provider only trips after fallback is exhausted and the provider request still fails with provider-wide transient errors; connection-scoped `429` rate limits stay in Connection Cooldown
-- **Wait For Cooldown** — The server can wait for the earliest connection cooldown to expire and retry the same client request automatically
-- **Anti-Thundering Herd** — Mutex + semaphore protection against concurrent retry storms
-- **Combo Fallback Chains** — If the primary provider fails, automatically falls through the chain with no intervention
-- **Health Dashboard** — Uptime monitoring, provider circuit breaker states, cooldowns, cache stats, p50/p95/p99 latency
+- **صف و سرعت درخواست** - سطل های درخواست هر اتصال قبل از اینکه به سقف های نرخ بالادستی برسند، یکنواخت می شوند
+- ** خنک کننده اتصال ** - یک اتصال واحد پس از خرابی های قابل امتحان مجدد با نکات اختیاری بالادست `Retry-After` و عقب نشینی نمایی خنک می شود
+- ** مدار شکن ارائه دهنده** - ارائه دهنده فقط پس از اتمام بازگشت مجدد و درخواست ارائه دهنده با خطاهای گذرا در سراسر ارائه دهنده با شکست مواجه می شود. محدودیتهای سرعت `429` با محدوده اتصال، در Cooldown اتصال باقی میمانند
+- **Wait For Cooldown** - سرور می تواند منتظر بماند تا اولین خنک شدن اتصال منقضی شود و دوباره همان درخواست مشتری را به طور خودکار امتحان کند.
+- ** گله ضد رعد ** - محافظت موتکس + سمافور در برابر طوفان های تکراری همزمان
+- ** زنجیره های بازگشتی ترکیبی ** - اگر ارائه دهنده اصلی شکست بخورد، به طور خودکار بدون مداخله از طریق زنجیره می افتد
+- ** داشبورد سلامت ** - نظارت بر زمان، وضعیت های قطع کننده مدار ارائه دهنده، خنک شدن، آمار حافظه پنهان، تاخیر p50/p95/p99
-🔧 7. "Configuring each AI tool is tedious and repetitive"
+🔧 ۷. «پیکربندی تکتک ابزارهای هوش مصنوعی خستهکننده و تکراری است»
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **CLI Tools Dashboard** — Dedicated page with one-click setup for Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline
-- **GitHub Copilot Config Generator** — Generates `chatLanguageModels.json` for VS Code with bulk model selection
-- **Onboarding Wizard** — Guided 4-step setup for first-time users
-- **One endpoint, all models** — Configure `http://localhost:20128/v1` once, access 329 provider catalog entries
+- **داشبورد ابزارهای CLI** — صفحهای اختصاصی با راهاندازی تککلیکی برای Claude Code، Codex CLI، OpenClaw، Kilo Code، Antigravity و Cline
+- **GitHub Copilot Config Generator** — `chatLanguageModels.json` را برای کد VS با انتخاب مدل انبوه تولید می کند
+- **جادوگر سوار شدن** - راه اندازی 4 مرحله ای هدایت شده برای کاربرانی که اولین بار هستند
+- **یک نقطه پایانی، همه مدل ها** - یک بار `http://localhost:20128/v1` را پیکربندی کنید، به 329 ورودی کاتالوگ ارائه دهنده دسترسی داشته باشید
-🔑 8. "Managing OAuth tokens from multiple providers is hell"
+🔑 ۸. «مدیریت توکنهای OAuth چندین ارائهدهنده بسیار دشوار است»
-Claude Code, Codex, Copilot — all use OAuth 2.0 with expiring tokens. Developers need to re-authenticate constantly, deal with `client_secret is missing`, `redirect_uri_mismatch`, and failures on remote servers. OAuth on LAN/VPS is particularly problematic.
+کد Claude، Codex، Copilot — همه از OAuth 2.0 با توکن های در حال انقضا استفاده می کنند. توسعهدهندگان باید دائماً احراز هویت مجدد کنند، با `client_secret is missing`، `redirect_uri_mismatch` و خرابیهای سرورهای راه دور مقابله کنند. OAuth در LAN/VPS به ویژه مشکل ساز است.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Auto Token Refresh** — OAuth tokens refresh in background before expiration
-- **OAuth 2.0 (PKCE) Built-in** — Automatic flow for Claude Code, Codex, Copilot, Kiro, Qwen, Qoder
-- **Multi-Account OAuth** — Multiple accounts per provider via JWT/ID token extraction
-- **OAuth LAN/Remote Fix** — Private IP detection for `redirect_uri` + manual URL mode for remote servers
-- **OAuth Behind Nginx** — Uses `window.location.origin` for reverse proxy compatibility
-- **Remote OAuth Guide** — Step-by-step guide for Google Cloud credentials on VPS/Docker
+- **بازسازی خودکار توکن** - توکن های OAuth قبل از انقضا در پس زمینه به روز می شوند
+- **OAuth 2.0 (PKCE) داخلی** — جریان خودکار برای Claude Code، Codex، Copilot، Kiro، Qwen و Qoder
+- ** چند حساب OAuth ** - چندین حساب در هر ارائه دهنده از طریق استخراج رمز JWT/ID
+- **OAuth LAN/Remote Fix** - تشخیص IP خصوصی برای `redirect_uri` + حالت دستی URL برای سرورهای راه دور
+- **OAuth پشت Nginx** — از `window.location.origin` برای سازگاری با پراکسی معکوس استفاده می کند
+- **راهنمای راه دور OAuth** - راهنمای گام به گام اعتبارنامه Google Cloud در VPS/Docker
-📊 9. "I don't know how much I'm spending or where"
+📊 ۹. «نمیدانم چقدر و برای چه چیزی هزینه میکنم»
-Developers use multiple paid providers but have no unified view of spending. Each provider has its own billing dashboard, but there's no consolidated view. Unexpected costs can pile up.
+توسعهدهندگان از چندین ارائهدهنده پولی استفاده میکنند، اما دیدگاه واحدی از هزینهها ندارند. هر ارائه دهنده داشبورد صورتحساب خود را دارد، اما هیچ نمای تلفیقی وجود ندارد. هزینه های غیرمنتظره می تواند روی هم انباشته شود.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Cost Analytics Dashboard** — Per-token cost tracking and budget management per provider
-- **Budget Limits per Tier** — Spending ceiling per tier that triggers automatic fallback
-- **Per-Model Pricing Configuration** — Configurable prices per model
-- **Usage Statistics Per API Key** — Request count and last-used timestamp per key
-- **Analytics Dashboard** — Stat cards, model usage chart, provider table with success rates and latency
+- **داشبورد تجزیه و تحلیل هزینه** - ردیابی هزینه هر توکن و مدیریت بودجه به ازای هر ارائه دهنده
+- **محدودیت بودجه در هر ردیف** - سقف هزینه در هر ردیف که باعث بازگشت خودکار می شود
+- **پیکربندی قیمت گذاری برای هر مدل** - قیمت های قابل تنظیم برای هر مدل
+- **آمار استفاده به ازای کلید API** — تعداد درخواست و آخرین مهر زمانی استفاده شده در هر کلید
+- **داشبورد تجزیه و تحلیل** - کارت های آمار، نمودار استفاده از مدل، جدول ارائه دهنده با میزان موفقیت و تاخیر
-🐛 10. "I can't diagnose errors and problems in AI calls"
+🐛 ۱۰. «نمیتوانم خطاها و مشکلات فراخوانیهای هوش مصنوعی را عیبیابی کنم»
-When a call fails, the dev doesn't know if it was a rate limit, expired token, wrong format, or provider error. Fragmented logs across different terminals. Without observability, debugging is trial-and-error.
+وقتی یک تماس با شکست مواجه میشود، برنامهنویس نمیداند که آیا محدودیت نرخ، رمز منقضی شده، فرمت اشتباه یا خطای ارائهدهنده بوده است. لاگ های تکه تکه شده در پایانه های مختلف. بدون قابلیت مشاهده، اشکال زدایی آزمون و خطا است.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Unified Logs Dashboard** — 4 tabs: Request Logs, Proxy Logs, Audit Logs, Console
-- **Console Log Viewer** — Real-time terminal-style viewer with color-coded levels, auto-scroll, search, filter
-- **SQLite Summary Logs** — Request and proxy log indexes stay queryable across restarts without loading large payload blobs into SQLite
-- **Translator Playground** — 4 debugging modes: Playground (format translation), Chat Tester (round-trip), Test Bench (batch), Live Monitor (real-time)
-- **Request Telemetry** — p50/p95/p99 latency + X-Request-Id tracing
-- **File-Based Detail Artifacts** — App logs rotate by size, retention days, and archive count; detailed request/response payloads live in `DATA_DIR/call_logs/` and rotate independently of SQLite summaries
-- **System Info Report** — `npm run system-info` generates `system-info.txt` with your full environment (Node version, OmniRoute version, OS, CLI tools, Docker/PM2 status). Attach it when reporting issues for instant triage.
+- **داشبورد گزارش های یکپارچه** - 4 تب: گزارش های درخواست، گزارش های پروکسی، گزارش های حسابرسی، کنسول
+- **نمایشگر ورود به سیستم** - نمایشگر به سبک ترمینال در زمان واقعی با سطوح رنگی، اسکرول خودکار، جستجو، فیلتر
+- **SQLite Summary Logs** - فهرست درخواست و پروکسی در طول راه اندازی مجدد بدون بارگیری حباب های بار بزرگ در SQLite قابل پرس و جو می ماند.
+- ** مترجم زمین بازی ** - 4 حالت اشکال زدایی: زمین بازی (ترجمه فرمت)، تستر چت (رفت و برگشت)، میز تست (دسته ای)، مانیتور زنده (زمان واقعی)
+- **تله متری درخواست** — تأخیر p50/p95/p99 + ردیابی X-Request-Id
+- ** مصنوعات جزئیات مبتنی بر فایل ** - سیاهههای مربوط به برنامه بر اساس اندازه، روزهای نگهداری و تعداد آرشیو می چرخند. بارهای درخواست/پاسخ دقیق در `DATA_DIR/call_logs/` زندگی می کنند و مستقل از خلاصه های SQLite می چرخند
+- **گزارش اطلاعات سیستم** - `npm run system-info` `system-info.txt` را با محیط کامل شما تولید می کند (نسخه Node، نسخه OmniRoute، سیستم عامل، ابزار CLI، وضعیت Docker/PM2). هنگام گزارش مشکلات برای تریاژ فوری، آن را ضمیمه کنید.
-🏗️ 11. "Deploying and maintaining the gateway is complex"
+🏗️ ۱۱. «استقرار و نگهداری درگاه پیچیده است»
-Installing, configuring, and maintaining an AI proxy across different environments (local, VPS, Docker, cloud) is labor-intensive. Problems like hardcoded paths, `EACCES` on directories, port conflicts, and cross-platform builds add friction.
+نصب، پیکربندی و نگهداری یک پروکسی هوش مصنوعی در محیط های مختلف (محلی، VPS، Docker، ابر) کار فشرده ای است. مشکلاتی مانند مسیرهای کدگذاری شده، `EACCES` در دایرکتوریها، تداخل پورتها، و ساختهای بین پلتفرمی باعث ایجاد اصطکاک میشوند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **npm global install** — `npm install -g omniroute && omniroute` — done
-- **Docker Multi-Platform** — AMD64 + ARM64 native (Apple Silicon, AWS Graviton, Raspberry Pi)
-- **Docker Compose Profiles** — `base` (no CLI tools) and `cli` (with Claude Code, Codex, OpenClaw)
-- **Electron Desktop App** — Native app for Windows/macOS/Linux with system tray, auto-start, offline mode
-- **Split-Port Mode** — API and Dashboard on separate ports for advanced scenarios (reverse proxy, container networking)
-- **Cloud Sync** — Config synchronization across devices via Cloudflare Workers
-- **DB Backups** — Automatic backup, restore, export and import of all settings, with `DISABLE_SQLITE_AUTO_BACKUP` for externally managed backups
+- ** نصب جهانی npm ** — `npm install -g omniroute && omniroute` — انجام شد
+- **Docker چند پلتفرم** — AMD64 + ARM64 بومی (Apple Silicon، AWS Graviton، Raspberry Pi)
+- **پروفایلهای Docker Compose** — `base` (بدون ابزار CLI) و `cli` (همراه Claude Code، Codex و OpenClaw)
+- **برنامه Electron Desktop** - برنامه بومی برای Windows/macOS/Linux با سینی سیستم، شروع خودکار، حالت آفلاین
+- ** حالت Split-Port ** - API و داشبورد در پورت های جداگانه برای سناریوهای پیشرفته (پراکسی معکوس، شبکه کانتینری)
+- **Cloud Sync** - همگام سازی پیکربندی بین دستگاه ها از طریق Cloudflare Workers
+- **پشتیبان گیری از DB** - پشتیبان گیری خودکار، بازیابی، صادرات و واردات تمام تنظیمات، با `DISABLE_SQLITE_AUTO_BACKUP` برای پشتیبان گیری های مدیریت شده خارجی
-🌍 12. "The interface is English-only and my team doesn't speak English"
+🌍 ۱۲. «رابط فقط انگلیسی است و تیم من انگلیسی صحبت نمیکند»
-Teams in non-English-speaking countries, especially in Latin America, Asia, and Europe, struggle with English-only interfaces. Language barriers reduce adoption and increase configuration errors.
+تیم های کشورهای غیر انگلیسی زبان، به ویژه در آمریکای لاتین، آسیا و اروپا، با رابط های فقط انگلیسی مبارزه می کنند. موانع زبان پذیرش را کاهش می دهد و خطاهای پیکربندی را افزایش می دهد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Dashboard i18n — 30 Languages** — All 500+ keys translated including Arabic, Bulgarian, Danish, German, Spanish, Finnish, French, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Malay, Dutch, Norwegian, Polish, Portuguese (PT/BR), Romanian, Russian, Slovak, Swedish, Thai, Ukrainian, Vietnamese, Chinese, Filipino, English
-- **RTL Support** — Right-to-left support for Arabic and Hebrew
-- **Multi-Language READMEs** — 30 complete documentation translations
-- **Language Selector** — Globe icon in header for real-time switching
+- **داشبورد i18n - 30 زبان** - تمام 500+ کلید ترجمه شده از جمله عربی، بلغاری، دانمارکی، آلمانی، اسپانیایی، فنلاندی، فرانسوی، عبری، هندی، مجارستانی، اندونزیایی، ایتالیایی، ژاپنی، کره ای، مالایی، هلندی، نروژی، لهستانی، پرتغالی (PT/BR)، رومانیایی، روسی، اسلواکی، سوئدی، تایلندی، اوکراینی، ویتنامی، چینی، فیلیپینی، انگلیسی
+- ** پشتیبانی RTL ** - پشتیبانی از راست به چپ برای عربی و عبری
+- ** README های چند زبانه ** - 30 ترجمه مستند کامل
+- ** انتخابگر زبان ** - نماد کره در هدر برای تغییر زمان واقعی
-🔄 13. "I need more than chat — I need embeddings, images, audio"
+🔄 ۱۳. «فراتر از چت نیاز دارم؛ به جاسازی، تصویر و صوت نیاز دارم»
-AI isn't just chat completion. Devs need to generate images, transcribe audio, create embeddings for RAG, rerank documents, and moderate content. Each API has a different endpoint and format.
+هوش مصنوعی فقط تکمیل چت نیست. توسعه دهندگان نیاز به تولید تصاویر، رونویسی صدا، ایجاد جاسازی برای RAG، رتبه بندی مجدد اسناد، و تعدیل محتوا دارند. هر API نقطه پایانی و قالب متفاوتی دارد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Embeddings** — `/v1/embeddings` with 6 providers and 9+ models
-- **Image Generation** — `/v1/images/generations` with 10 providers and 20+ models (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigravity, SD WebUI, ComfyUI)
-- **Text-to-Video** — `/v1/videos/generations` — ComfyUI (AnimateDiff, SVD) and SD WebUI
-- **Text-to-Music** — `/v1/music/generations` — ComfyUI (Stable Audio Open, MusicGen)
-- **Audio Transcription** — `/v1/audio/transcriptions` — Whisper + Nvidia NIM, HuggingFace, Qwen3
-- **Text-to-Speech** — `/v1/audio/speech` — ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3, **Inworld**, **Cartesia**, **PlayHT**, + existing providers
-- **Moderations** — `/v1/moderations` — Content safety checks
-- **Reranking** — `/v1/rerank` — Document relevance reranking
-- **Responses API** — Full `/v1/responses` support for Codex
+- **جاسازی ها** — `/v1/embeddings` با 6 ارائه دهنده و 9+ مدل
+- **تولید تصویر** — `/v1/images/generations` با 10 ارائه دهنده و 20+ مدل (OpenAI، xAI، Together، Fireworks، Nebius، Hyperbolic، NanoBanana، Antigravity، SD WebUI، ComfyUI)
+- **تکست به ویدئو** - `/v1/videos/generations` - ComfyUI (AnimateDiff، SVD) و SD WebUI
+- **تکست به موسیقی** - `/v1/music/generations` - ComfyUI (باز صدای پایدار، MusicGen)
+- **رونویسی صوتی** - `/v1/audio/transcriptions` - Whisper + Nvidia NIM، HuggingFace، Qwen3
+- **تکست به گفتار** — `/v1/audio/speech` — ElevenLabs، Nvidia NIM، HuggingFace، Coqui، Tortoise، Qwen3، **Inworld**، **Cartesia**، **PlayHT**، + ارائه دهندگان موجود
+- ** تعدیل ها ** - `/v1/moderations` - بررسی های ایمنی محتوا
+- **رتبهبندی مجدد** - `/v1/rerank` - رتبهبندی مجدد مربوط به سند
+- **پاسخ API** — پشتیبانی کامل از `/v1/responses` برای Codex
-🧪 14. "I have no way to test and compare quality across models"
+🧪 ۱۴. «راهی برای آزمودن و مقایسهٔ کیفیت مدلها ندارم»
-Developers want to know which model is best for their use case — code, translation, reasoning — but comparing manually is slow. No integrated eval tools exist.
+توسعهدهندگان میخواهند بدانند کدام مدل برای موارد استفاده آنها بهترین است - کد، ترجمه، استدلال - اما مقایسه دستی کند است. هیچ ابزار ارزیابی یکپارچه وجود ندارد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **LLM Evaluations** — Golden set testing with 10 pre-loaded cases covering greetings, math, geography, code generation, JSON compliance, translation, markdown, safety refusal
-- **4 Match Strategies** — `exact`, `contains`, `regex`, `custom` (JS function)
-- **Translator Playground Test Bench** — Batch testing with multiple inputs and expected outputs, cross-provider comparison
-- **Chat Tester** — Full round-trip with visual response rendering
-- **Live Monitor** — Real-time stream of all requests flowing through the proxy
+- ** ارزیابی های LLM ** - تست مجموعه طلایی با 10 مورد از پیش بارگذاری شده که احوالپرسی، ریاضی، جغرافیا، تولید کد، انطباق با JSON، ترجمه، علامت گذاری، امتناع ایمنی را پوشش می دهد
+- **4 استراتژی مطابقت ** — `exact`، `contains`، `regex`، `custom` (عملکرد JS)
+- **نیمکت تست مترجم زمین بازی** - تست دسته ای با ورودی های متعدد و خروجی های مورد انتظار، مقایسه بین ارائه دهندگان
+- **تستر چت** - رفت و برگشت کامل با رندر پاسخ بصری
+- ** مانیتور زنده ** - جریان بیدرنگ تمام درخواست هایی که از طریق پروکسی جریان می یابد
-📈 15. "I need to scale without losing performance"
+📈 ۱۵. «باید بدون افت کارایی مقیاسپذیر شوم»
-As request volume grows, without caching the same questions generate duplicate costs. Without idempotency, duplicate requests waste processing. Per-provider rate limits must be respected.
+با افزایش حجم درخواست، بدون ذخیره سازی در حافظه پنهان، همان سوالات هزینه های تکراری ایجاد می کنند. بدون ناتوانی، درخواست های تکراری پردازش زباله. محدودیت های نرخ هر ارائه دهنده باید رعایت شود.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Semantic Cache** — Two-tier cache (signature + semantic) reduces cost and latency
-- **Request Idempotency** — 5s deduplication window for identical requests
-- **Rate Limit Detection** — Per-provider RPM, min gap, and max concurrent tracking
-- **Request Queue & Pacing** — Configurable queue, pacing, and concurrency defaults in Settings → Resilience
-- **API Key Validation Cache** — 3-tier cache for production performance
-- **Health Dashboard with Telemetry** — p50/p95/p99 latency, cache stats, uptime
+- ** کش معنایی ** - کش دو لایه (امضا + معنایی) هزینه و تأخیر را کاهش می دهد
+- ** درخواست Idempotency ** - پنجره deduplication 5s برای درخواست های یکسان
+- **تشخیص محدودیت نرخ** - RPM هر ارائه دهنده، حداقل فاصله و حداکثر ردیابی همزمان
+- **درخواست صف و سرعت** - پیش فرض های صف، سرعت و همزمانی قابل تنظیم در تنظیمات → انعطاف پذیری
+- ** حافظه پنهان اعتبارسنجی کلید API ** - حافظه نهان 3 لایه برای عملکرد تولید
+- ** داشبورد سلامت با تله متری ** - تأخیر p50/p95/p99، آمار حافظه پنهان، زمان آپدیت
-🤖 16. "I want to control model behavior globally"
+🤖 ۱۶. «میخواهم رفتار مدل را بهصورت سراسری کنترل کنم»
-Developers who want all responses in a specific language, with a specific tone, or want to limit reasoning tokens. Configuring this in every tool/request is impractical.
+توسعه دهندگانی که همه پاسخ ها را به زبانی خاص، با لحن خاصی می خواهند یا می خواهند نشانه های استدلال را محدود کنند. پیکربندی این در هر ابزار/درخواست غیرعملی است.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **System Prompt Injection** — Global prompt applied to all requests
-- **Thinking Budget Validation** — Reasoning token allocation control per request (passthrough, auto, custom, adaptive)
-- **9 Routing Strategies** — Global strategies that determine how requests are distributed
-- **Wildcard Router** — `provider/*` patterns route dynamically to any provider
-- **Combo Enable/Disable Toggle** — Toggle combos directly from the dashboard
-- **Manual Combo Ordering** — Drag combo cards by handle and persist the order in SQLite
-- **Provider Toggle** — Enable/disable all connections for a provider with one click
-- **Blocked Providers** — Exclude specific providers from `/v1/models` listing
+- **تزریق سریع سیستم** - اعلان جهانی برای همه درخواست ها اعمال می شود
+- ** اعتبارسنجی بودجه فکری ** - کنترل تخصیص رمز استدلال در هر درخواست (گذرا، خودکار، سفارشی، تطبیقی)
+- **9 استراتژی مسیریابی** - استراتژی های جهانی که نحوه توزیع درخواست ها را تعیین می کند
+- **مسیریاب Wildcard** - الگوهای `provider/*` به صورت پویا به هر ارائه دهنده ای می روند
+- ** Combo Enable/Disable Toggle** - جابجایی ترکیبی به طور مستقیم از داشبورد
+- ** سفارش دستی ترکیبی ** - کارت های ترکیبی را با دسته بکشید و سفارش را در SQLite ادامه دهید
+- **تغییر ارائه دهنده** - همه اتصالات یک ارائه دهنده را با یک کلیک فعال/غیرفعال کنید
+- **ارائه دهندگان مسدود شده** - ارائه دهندگان خاص را از لیست `/v1/models` حذف کنید
-🧰 17. "I need MCP tools as first-class product capabilities"
+🧰 ۱۷. «به ابزارهای MCP بهعنوان قابلیتهای اصلی محصول نیاز دارم»
-Many AI gateways expose MCP only as a hidden implementation detail. Teams need a visible, manageable operation layer.
+بسیاری از دروازههای هوش مصنوعی MCP را تنها به عنوان یک جزئیات پیادهسازی پنهان نشان میدهند. تیم ها به یک لایه عملیاتی قابل کنترل و قابل مشاهده نیاز دارند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- MCP appears in the dashboard navigation and endpoint protocol tab
-- Dedicated MCP management page with process, tools, scopes, and audit
-- Built-in quick-start for `omniroute --mcp` and client onboarding
+- MCP در برگه ناوبری داشبورد و پروتکل نقطه پایان ظاهر می شود
+- صفحه مدیریت اختصاصی MCP با فرآیند، ابزار، دامنه و ممیزی
+- راه اندازی سریع داخلی برای `omniroute --mcp` و نصب مشتری
-🧠 18. "I need A2A orchestration with sync + stream task paths"
+🧠 ۱۸. «به ارکستراسیون A2A با مسیرهای وظیفهٔ همگام و جریانی نیاز دارم»
-Agent workflows need both direct replies and long-running streamed execution with lifecycle control.
+گردش کار عامل هم به پاسخ های مستقیم و هم به اجرای جریانی طولانی مدت با کنترل چرخه حیات نیاز دارد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- A2A JSON-RPC endpoint (`POST /a2a`) with `message/send` and `message/stream`
-- SSE streaming with terminal state propagation
-- Task lifecycle APIs for `tasks/get` and `tasks/cancel`
+- نقطه پایانی A2A JSON-RPC (`POST /a2a`) با `message/send` و `message/stream`
+- پخش جریانی SSE با انتشار حالت ترمینال
+- APIهای چرخه حیات وظیفه برای `tasks/get` و `tasks/cancel`
-🛰️ 19. "I need real MCP process health, not guessed status"
+🛰️ ۱۹. «به سلامت واقعی فرایند MCP نیاز دارم، نه وضعیت حدسی»
-Operational teams need to know if MCP is actually alive, not just whether an API is reachable.
+تیم های عملیاتی باید بدانند که آیا MCP واقعاً زنده است یا نه، نه فقط اینکه آیا API قابل دسترسی است یا خیر.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Runtime heartbeat file with PID, timestamps, transport, tool count, and scope mode
-- MCP status API combining heartbeat + recent activity
-- UI status cards for process/uptime/heartbeat freshness
+- فایل ضربان قلب در زمان اجرا با PID، مهرهای زمانی، حمل و نقل، شمارش ابزار و حالت دامنه
+- وضعیت MCP API ترکیبی از ضربان قلب + فعالیت اخیر
+- کارت های وضعیت رابط کاربری برای تازگی فرآیند/تایم/ضربان قلب
-📋 20. "I need auditable MCP tool execution"
+📋 ۲۰. «به اجرای قابلممیزی ابزارهای MCP نیاز دارم»
-When tools mutate config or trigger ops actions, teams need forensic traceability.
+هنگامی که ابزارها پیکربندی را تغییر می دهند یا اقدامات عملیاتی را آغاز می کنند، تیم ها به قابلیت ردیابی قانونی نیاز دارند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- SQLite-backed audit logging for MCP tool calls
-- Filters by tool, success/failure, API key, and pagination
-- Dashboard audit table + stats endpoints for automation
+- ثبت حسابرسی با پشتیبانی SQLite برای تماس های ابزار MCP
+- فیلترها بر اساس ابزار، موفقیت/شکست، کلید API و صفحه بندی
+- جدول حسابرسی داشبورد + نقاط پایانی آمار برای اتوماسیون
-🔐 21. "I need scoped MCP permissions per integration"
+🔐 ۲۱. «برای هر یکپارچهسازی، به مجوزهای محدودهدار MCP نیاز دارم»
-Different clients should have least-privilege access to tool categories.
+مشتریان مختلف باید کمترین امتیاز را به دسته های ابزار داشته باشند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- 32 granular MCP scopes for controlled tool access
-- Scope enforcement and visibility in MCP management UI
-- Safe default posture for operational tooling
+- 32 اسکوپ گرانول MCP برای دسترسی کنترل شده به ابزار
+- اجرای محدوده و دید در رابط کاربری مدیریت MCP
+- وضعیت پیش فرض ایمن برای ابزار عملیاتی
-⚙️ 22. "I need operational controls without redeploying"
+⚙️ ۲۲. «به کنترلهای عملیاتی بدون استقرار مجدد نیاز دارم»
-Teams need quick runtime changes during incidents or cost events.
+تیم ها به تغییرات سریع در زمان اجرا در طول حوادث یا رویدادهای هزینه نیاز دارند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Switch combo activation directly from MCP dashboard
-- Tune queue, cooldown, breaker, and wait settings from the dedicated Resilience page
-- Review live provider breaker state from the Health dashboard
+- فعال سازی ترکیبی را مستقیماً از داشبورد MCP تغییر دهید
+- تنظیمات صف، خنک کننده، شکن و انتظار را از صفحه اختصاصی Resilience تنظیم کنید
+- وضعیت قطع کننده ارائه دهنده زنده را از داشبورد Health مرور کنید
-🔄 23. "I need live A2A task lifecycle visibility and cancellation"
+🔄 ۲۳. «به مشاهدهٔ زنده و لغو چرخهٔ حیات وظایف A2A نیاز دارم»
-Without lifecycle visibility, task incidents become hard to triage.
+بدون دید چرخه حیات، تریاژ حوادث کار سخت می شود.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Task listing/filtering by state/skill with pagination
-- Drill-down on task metadata, events, and artifacts
-- Task cancellation endpoint and UI action with confirmation
+- فهرست کار/فیلتر کردن بر اساس وضعیت/مهارت با صفحه بندی
+- متادیتاهای وظیفه، رویدادها و مصنوعات را بررسی کنید
+- نقطه پایانی لغو کار و اقدام UI با تأیید
-🌊 24. "I need active stream metrics for A2A load"
+🌊 ۲۴. «برای بار A2A به معیارهای جریان فعال نیاز دارم»
-Streaming workflows require operational insight into concurrency and live connections.
+جریان کار مستلزم بینش عملیاتی در مورد همزمانی و اتصالات زنده است.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Active stream counters integrated into A2A status
-- Last task timestamp and per-state counts
-- A2A dashboard cards for real-time ops monitoring
+- شمارنده های جریان فعال در وضعیت A2A یکپارچه شده است
+- آخرین مهر زمان کار و تعداد هر ایالت
+- کارت های داشبورد A2A برای نظارت بر عملیات در زمان واقعی
-🪪 25. "I need standard agent discovery for clients"
+🪪 ۲۵. «به کشف استاندارد عامل برای کلاینتها نیاز دارم»
-External clients and orchestrators need machine-readable metadata for onboarding.
+مشتریان خارجی و ارکستراتورها برای سوار شدن به ابرداده قابل خواندن توسط ماشین نیاز دارند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Agent Card exposed at `/.well-known/agent.json`
-- Capabilities and skills shown in management UI
-- A2A status API includes discovery metadata for automation
+- کارت نماینده در `/.well-known/agent.json` در معرض دید قرار گرفت
+- قابلیت ها و مهارت های نشان داده شده در رابط کاربری مدیریت
+- وضعیت A2A API شامل ابرداده های کشف برای اتوماسیون است
-🧭 26. "I need protocol discoverability in the product UX"
+🧭 ۲۶. «به کشفپذیری پروتکل در تجربهٔ کاربری محصول نیاز دارم»
-If users cannot discover protocol surfaces, adoption and support quality drop.
+اگر کاربران نتوانند سطوح پروتکل را کشف کنند، کیفیت پذیرش و پشتیبانی کاهش می یابد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Consolidated **Endpoints** page with tabs for Proxy, MCP, A2A, and API Endpoints
-- Inline service status toggles (Online/Offline) for MCP and A2A
-- Links from overview to dedicated management tabs
+- صفحه تلفیقی **Endpoints** با برگههای Proxy، MCP، A2A، و API
+- تغییر وضعیت سرویس درون خطی (آنلاین/آفلاین) برای MCP و A2A
+- پیوند از نمای کلی به برگه های مدیریت اختصاصی
-🧪 27. "I need end-to-end protocol validation with real clients"
+🧪 ۲۷. «به اعتبارسنجی سرتاسری پروتکل با کلاینتهای واقعی نیاز دارم»
-Mock tests are not enough to validate protocol compatibility before release.
+تست های ساختگی برای تایید سازگاری پروتکل قبل از انتشار کافی نیستند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- E2E suite that boots app and uses real MCP SDK client transport
-- A2A client tests for discovery, send, stream, get, and cancel flows
-- Cross-check assertions against MCP audit and A2A tasks APIs
+- مجموعه E2E که برنامه را بوت می کند و از حمل و نقل مشتری واقعی MCP SDK استفاده می کند
+- تست مشتری A2A برای کشف، ارسال، پخش، دریافت و لغو جریان ها
+- بررسی متقاطع ادعاها علیه MCP ممیزی و API وظایف A2A
-📡 28. "I need unified observability across all interfaces"
+📡 ۲۸. «به مشاهدهپذیری یکپارچه در همهٔ رابطها نیاز دارم»
-Splitting observability by protocol creates blind spots and longer MTTR.
+تقسیم قابلیت مشاهده توسط پروتکل باعث ایجاد نقاط کور و MTTR طولانی تر می شود.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Unified dashboards/logs/analytics in one product
-- Health + audit + request telemetry across OpenAI, MCP, and A2A layers
-- Operational APIs for status and automation
+- داشبورد / سیاهههای مربوط / تجزیه و تحلیل یکپارچه در یک محصول
+- سلامت + ممیزی + درخواست تله متری در لایه های OpenAI، MCP، و A2A
+- API های عملیاتی برای وضعیت و اتوماسیون
-💼 29. "I need one runtime for proxy + tools + agent orchestration"
+💼 ۲۹. «برای پروکسی، ابزارها و ارکستراسیون عامل به یک محیط اجرا نیاز دارم»
-Running many separate services increases operational cost and failure modes.
+اجرای بسیاری از خدمات جداگانه هزینه عملیاتی و حالت های خرابی را افزایش می دهد.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- OpenAI-compatible proxy, MCP server, and A2A server in one stack
-- Shared auth, resilience, data store, and observability
-- Consistent policy model across all interaction surfaces
+- پروکسی سازگار با OpenAI، سرور MCP و سرور A2A در یک پشته
+- اعتبار مشترک، انعطاف پذیری، ذخیره داده ها و قابلیت مشاهده
+- مدل خط مشی سازگار در تمام سطوح تعامل
-🚀 30. "I need to ship agentic workflows without glue-code sprawl"
+🚀 ۳۰. «میخواهم گردشکارهای عاملمحور را بدون پراکندگی کدهای اتصالدهنده منتشر کنم»
-Teams lose velocity when stitching multiple ad-hoc services and scripts.
+تیم ها هنگام دوخت چندین سرویس ad-hoc و اسکریپت سرعت خود را از دست می دهند.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- Unified endpoint strategy for clients and agents
-- Built-in protocol management UIs and smoke validation paths
-- Production-ready foundations (security, logging, resilience, backup)
+- استراتژی نقطه پایانی یکپارچه برای مشتریان و نمایندگان
+- رابط های کاربری داخلی مدیریت پروتکل و مسیرهای اعتبارسنجی دود
+- پایه های آماده تولید (امنیت، ورود به سیستم، انعطاف پذیری، پشتیبان گیری)
-📚 31. "My long sessions crash with 'context_length_exceeded' limits"
+📚 ۳۱. «نشستهای طولانی من با محدودیت «context_length_exceeded» متوقف میشوند»
-During deep debugging, long histories with tool results quickly exceed provider token windows, causing failed requests and orphaned context.
+در طول اشکالزدایی عمیق، تاریخچههای طولانی با نتایج ابزار به سرعت از پنجرههای رمز ارائهدهنده فراتر میروند، که باعث درخواستهای ناموفق و زمینه بیاطلاعی میشود.
-**How OmniRoute solves it:**
+** چگونه OmniRoute آن را حل می کند:**
-- **Proactive Context Compression** — Evaluates token budgets before the request hits upstream and proactively prunes old conversation history with a smart binary-search mechanism.
-- **Structural Integrity Guards** — Automatically tracks explicit `tool_use` definitions and ensures that if a tool input is truncated, its corresponding `tool_result` is also safely removed, preventing API validation errors.
-- **Multi-Layer Dropping** — Progressively drops system messages, regular messages, and finally enforces strict length limits without breaking conversational logic.
+- ** فشرده سازی متن پیشگیرانه ** - قبل از اینکه درخواست به بالادست برسد، بودجه توکن ها را ارزیابی می کند و به طور فعال تاریخچه مکالمات قدیمی را با مکانیزم جستجوی باینری هوشمند حذف می کند.
+- ** محافظ یکپارچگی ساختاری** - تعاریف صریح `tool_use` را به طور خودکار ردیابی می کند و تضمین می کند که اگر ورودی ابزار کوتاه شود، `tool_result` مربوطه آن نیز با خیال راحت حذف می شود و از خطاهای اعتبارسنجی API جلوگیری می کند.
+- **کاهش چند لایه** - به تدریج پیام های سیستمی، پیام های معمولی را حذف می کند و در نهایت محدودیت های طولانی مدت را بدون نقض منطق مکالمه اعمال می کند.
-### Example Playbooks (Integrated Use Cases)
+### نمونه کتابهای راهنما (مورد استفاده یکپارچه)
-**Playbook A: Maximize paid subscription + cheap backup**
+**Playbook A: اشتراک پولی را به حداکثر برسانید + پشتیبان گیری ارزان**
```txt
Combo: "maximize-claude"
@@ -696,7 +696,7 @@ Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
```
-**Playbook B: Zero-cost coding stack**
+**راهنمای B: پشته کدگذاری بدون هزینه**
```txt
Combo: "free-access"
@@ -707,7 +707,7 @@ Monthly cost: $0
Outcome: broader free-access fallback; upstream availability is not guaranteed
```
-**Playbook C: 24/7 always-on fallback chain**
+**کتاب راهنما C: زنجیره بازگشتی 24 ساعته همیشه فعال**
```txt
Combo: "multi-layer-fallback"
@@ -720,7 +720,7 @@ Combo: "multi-layer-fallback"
Outcome: deep fallback depth for deadline-critical workloads
```
-**Playbook D: Agent ops with MCP + A2A**
+**راهنمای D: عملیات عامل با MCP + A2A**
```txt
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
@@ -731,57 +731,57 @@ Outcome: deep fallback depth for deadline-critical workloads
---
-## 🆓 Start Free — Zero Configuration Cost
+## 🆓 شروع رایگان — هزینه پیکربندی صفر
-> Setup AI coding in minutes at **$0/month**. Connect these free accounts and use the built-in **Free Stack** combo.
+> کدگذاری هوش مصنوعی را در چند دقیقه با **0 دلار در ماه** تنظیم کنید. این حسابهای رایگان را متصل کنید و از ترکیب داخلی **پشته رایگان** استفاده کنید.
-| Step | Action | Providers Unlocked |
+| مرحله | اقدام | قفل ارائه دهندگان |
| ---- | -------------------------------------------------- | ------------------------------------------------------------------ |
-| 1 | Connect **Kiro** (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 — provider/account limits apply |
-| 2 | Connect **Qoder** (Google OAuth) | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1... — provider/account limits apply |
-| 3 | Connect **Qwen** (Device Code) | qwen3-coder-plus, qwen3-coder-flash... — provider/account limits apply |
-| 4 | `/dashboard/combos` → **Free Stack ($0)** template | Round-robin all free providers automatically |
+| 1 | اتصال **Kiro** (شناسه سازنده AWS OAuth) | Claude Sonnet 4.5، Haiku 4.5 — محدودیت های ارائه دهنده/حساب اعمال می شود |
+| 2 | اتصال **Qoder** (Google OAuth) | kimi-k2-thinking، qwen3-coder-plus، deepseek-r1... — محدودیت های ارائه دهنده/حساب اعمال می شود |
+| 3 | اتصال **Qwen** (کد دستگاه) | qwen3-coder-plus، qwen3-coder-flash... — محدودیت های ارائه دهنده/حساب اعمال می شود |
+| 4 | قالب `/dashboard/combos` → **پشته رایگان (0$)** | همه ارائه دهندگان رایگان را بصورت خودکار |
-**Point any IDE/CLI to:** `http://localhost:20128/v1` · API Key: `any-string` · Done.
+**کلید IDE/CLI را به:** `http://localhost:20128/v1` · API کلید: `any-string` · انجام شد.
-> **Optional extra coverage (current terms apply):** Groq, NVIDIA NIM, Cerebras, LongCat and Cloudflare Workers AI can provide free access or signup credits where currently listed. Quotas, models, accounts, regions and provider terms can change; see [`FREE_TIERS.md`](../../reference/FREE_TIERS.md).
+> **پوشش اضافی اختیاری (شرایط فعلی اعمال میشود):** Groq، NVIDIA NIM، Cerebras، LongCat و Cloudflare Workers AI میتواند اعتبار رایگان را در مکانهایی که در حال حاضر فهرست شده است ارائه دهد. سهمیه ها، مدل ها، حساب ها، مناطق و شرایط ارائه دهنده می توانند تغییر کنند. [`FREE_TIERS.md`](../../reference/FREE_TIERS.md) را ببینید.
-## Inicio Rápido
+## اینیسیو راپیدو
-### 1) Install and run
+### 1) نصب و اجرا کنید
```bash
npm install -g omniroute
omniroute
```
-> **pnpm users:** Pass `--allow-build` at install time to enable native build scripts required by `better-sqlite3` and `@swc/core` (the `approve-builds -g` command is not supported for global installs on pnpm v11):
+> **کاربران pnpm:** در زمان نصب `--allow-build` را برای فعال کردن اسکریپت های ساخت بومی مورد نیاز `better-sqlite3` و `@swc/core` ارسال کنید (دستور `approve-builds -g` برای نصب های جهانی در pnpm v11 پشتیبانی نمی شود):
>
> ```bash
> pnpm add -g omniroute@latest --allow-build=better-sqlite3 --allow-build=@swc/core
> omniroute
> ```
-Dashboard opens at `http://localhost:20128` and API base URL is `http://localhost:20128/v1`.
+داشبورد در `http://localhost:20128` باز می شود و پایه API URL `http://localhost:20128/v1` است.
-#### Arch Linux (AUR)
+#### آرچ لینوکس (AUR)
-Arch Linux users can install the [AUR package](https://aur.archlinux.org/packages/omniroute-bin), which installs OmniRoute and provides a systemd user service:
+کاربران آرچ لینوکس می توانند [بسته AUR](https://aur.archlinux.org/packages/omniroute-bin) را نصب کنند که OmniRoute را نصب می کند و یک سرویس کاربر سیستمی ارائه می دهد:
```bash
yay -S omniroute-bin
systemctl --user enable --now omniroute.service
```
-| Command | Description |
+| فرمان | توضیحات |
| ----------------------- | ----------------------------------------------------------- |
-| `omniroute` | Start server (`PORT=20128`, API and dashboard on same port) |
-| `omniroute --port 3000` | Set canonical/API port to 3000 |
-| `omniroute --mcp` | Start MCP server (stdio transport) |
-| `omniroute --no-open` | Don't auto-open browser |
-| `omniroute --help` | Show help |
+| `omniroute` | راه اندازی سرور (`PORT=20128`، API و داشبورد در همان پورت) |
+| `omniroute --port 3000` | پورت canonical/API را روی 3000 | تنظیم کنید
+| `omniroute --mcp` | راه اندازی سرور MCP (stdio transport) |
+| `omniroute --no-open` | مرورگر خودکار باز نشود |
+| `omniroute --help` | نمایش کمک |
-Optional split-port mode:
+حالت اسپلیت پورت اختیاری:
```bash
PORT=20128 DASHBOARD_PORT=20129 omniroute
@@ -789,63 +789,63 @@ PORT=20128 DASHBOARD_PORT=20129 omniroute
# Dashboard: http://localhost:20129
```
-### 2) Uninstalling
+### 2) حذف نصب
-When you no longer need OmniRoute, we provide two quick scripts for a clean removal:
+هنگامی که دیگر نیازی به OmniRoute ندارید، ما دو اسکریپت سریع برای حذف تمیز ارائه می دهیم:
-| Command | Action |
+| فرمان | اقدام |
| ------------------------ | ----------------------------------------------------------------------------------- |
-| `npm run uninstall` | Removes the system app but **keeps your DB and configurations** in `~/.omniroute`. |
-| `npm run uninstall:full` | Removes the app AND permanently **erases all configurations, keys, and databases**. |
+| `npm run uninstall` | برنامه سیستم را حذف می کند اما **DB و تنظیمات** شما را در `~/.omniroute` نگه می دارد. |
+| `npm run uninstall:full` | برنامه را حذف می کند و برای همیشه **همه پیکربندی ها، کلیدها و پایگاه داده ها را پاک می کند**. |
-> Note: To run these commands, navigate to the OmniRoute project folder (if you cloned it) and run them. Alternatively, if globally installed, you can simply run `npm uninstall -g omniroute`.
+> توجه: برای اجرای این دستورات، به پوشه پروژه OmniRoute (اگر آن را شبیه سازی کرده اید) بروید و آنها را اجرا کنید. از طرف دیگر، اگر به صورت سراسری نصب شده باشد، می توانید به سادگی `npm uninstall -g omniroute` را اجرا کنید.
-### Long-Running Streaming Timeouts
+### وقفه های طولانی مدت استریم
-For most deployments, you only need:
+برای اکثر استقرارها، فقط نیاز دارید:
-| Variable | Default | Purpose |
+| متغیر | پیش فرض | هدف |
| ------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
-| `REQUEST_TIMEOUT_MS` | `600000` | Shared baseline for upstream response-start timeout, hidden Undici timeouts, TLS fingerprint requests, and API bridge request/proxy timeouts |
-| `STREAM_IDLE_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` | Maximum gap between streaming chunks before OmniRoute aborts the SSE stream |
+| `REQUEST_TIMEOUT_MS` | `600000` | خط پایه مشترک برای مهلت زمانی شروع پاسخ بالادست، مهلت زمانی پنهان Undici، درخواستهای اثر انگشت TLS و زمانبندی درخواست پل/پراکسی API |
+| `STREAM_IDLE_TIMEOUT_MS` | به ارث می برد `REQUEST_TIMEOUT_MS` | حداکثر فاصله بین تکه های جریان قبل از اینکه OmniRoute جریان SSE را لغو کند |
-Backward compatibility is preserved: existing `FETCH_TIMEOUT_MS`, `API_BRIDGE_PROXY_TIMEOUT_MS`, and other per-layer timeout vars still work and override the shared baseline.
+سازگاری به عقب حفظ می شود: `FETCH_TIMEOUT_MS`، `API_BRIDGE_PROXY_TIMEOUT_MS`، و سایر متغیرهای وقفه زمانی هر لایه هنوز کار می کنند و خط پایه مشترک را لغو می کنند.
-For Claude Code-compatible upstreams (`anthropic-compatible-cc-*`), OmniRoute also derives the outbound `X-Stainless-Timeout` header from the resolved fetch timeout so provider-side read timeouts stay aligned with your env configuration.
+برای Claude بالادستی سازگار با کد (`anthropic-compatible-cc-*`)، OmniRoute همچنین سرصفحه خروجی `X-Stainless-Timeout` را از بازه زمانی واکشی حلشده استخراج میکند، بنابراین زمانبندی خواندن سمت ارائهدهنده با پیکربندی env شما همسو میشود.
-For third-party Claude Code-compatible reverse proxies, OmniRoute keeps the default
-`anthropic-beta` set conservative and, when `Client Cache Control` is left on `Auto`,
-only forwards client-provided `cache_control` markers. If the request does not include
-`cache_control`, OmniRoute does not inject bridge-owned markers.
+برای پراکسی های معکوس Claude شخص ثالث سازگار با کد، OmniRoute پیش فرض را نگه می دارد
+`anthropic-beta` محافظه کارانه تنظیم می شود و وقتی `Client Cache Control` روی `Auto` باقی می ماند،
+فقط نشانگرهای `cache_control` ارائه شده توسط مشتری را فوروارد می کند. اگر درخواست شامل نمی شود
+`cache_control`، OmniRoute نشانگرهای متعلق به پل را تزریق نمی کند.
-Advanced overrides are available if you need finer control:
+در صورت نیاز به کنترل دقیق تر، لغو پیشرفته در دسترس است:
-| Variable | Default | Purpose |
+| متغیر | پیش فرض | هدف |
| ---------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------- |
-| `FETCH_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` | Upstream response-start timeout used until response headers arrive |
-| `FETCH_HEADERS_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Undici time limit for receiving upstream response headers |
-| `FETCH_BODY_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Undici time limit between upstream body chunks (`0` disables it) |
-| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | Undici TCP connect timeout |
-| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Undici idle keep-alive socket timeout |
-| `TLS_CLIENT_TIMEOUT_MS` | inherits `FETCH_TIMEOUT_MS` | Timeout for TLS fingerprint requests made through `wreq-js` |
-| `API_BRIDGE_PROXY_TIMEOUT_MS` | inherits `REQUEST_TIMEOUT_MS` or `600000` | Timeout for `/v1` proxy forwarding from API port to dashboard port |
-| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `max(API_BRIDGE_PROXY_TIMEOUT_MS, 300000)` | Incoming request timeout on the API bridge server |
-| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | Incoming header timeout on the API bridge server |
-| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Keep-alive timeout on the API bridge server |
-| `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | Socket inactivity timeout on the API bridge server (`0` disables it) |
+| `FETCH_TIMEOUT_MS` | به ارث می برد `REQUEST_TIMEOUT_MS` | زمان پایان پاسخ-شروع بالادستی تا رسیدن سرصفحه های پاسخ استفاده می شود |
+| `FETCH_HEADERS_TIMEOUT_MS` | به ارث می برد `FETCH_TIMEOUT_MS` | محدودیت زمانی Undici برای دریافت سرصفحه های پاسخ بالادست |
+| `FETCH_BODY_TIMEOUT_MS` | به ارث می برد `FETCH_TIMEOUT_MS` | محدودیت زمانی Undici بین تکه های بدنه بالادست (`0` آن را غیرفعال می کند) |
+| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | زمان اتصال Undici TCP |
+| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | مهلت زمانی سوکت بیکار نگه داشتن زنده Undici |
+| `TLS_CLIENT_TIMEOUT_MS` | به ارث می برد `FETCH_TIMEOUT_MS` | مهلت زمانی برای درخواست های اثر انگشت TLS که از طریق `wreq-js` |
+| `API_BRIDGE_PROXY_TIMEOUT_MS` | به ارث می برد `REQUEST_TIMEOUT_MS` یا `600000` | مهلت زمانی برای ارسال پروکسی `/v1` از پورت API به پورت داشبورد |
+| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `max(API_BRIDGE_PROXY_TIMEOUT_MS, 300000)` | زمان درخواست ورودی در سرور پل API |
+| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | مهلت زمانی هدر ورودی در سرور پل API |
+| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | وقفه ماندن زنده در سرور پل API |
+| `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | پایان زمان عدم فعالیت سوکت در سرور پل API (`0` آن را غیرفعال می کند) |
-For streaming requests, `FETCH_TIMEOUT_MS` only covers connection setup / waiting for the first upstream response. Once the stream is active, OmniRoute will only abort on an actual stall (`STREAM_IDLE_TIMEOUT_MS`) or Undici body inactivity (`FETCH_BODY_TIMEOUT_MS`).
+برای درخواستهای پخش جریانی، `FETCH_TIMEOUT_MS` فقط راهاندازی اتصال / انتظار برای اولین پاسخ بالادستی را پوشش میدهد. پس از فعال شدن جریان، OmniRoute فقط در حالت توقف واقعی (`STREAM_IDLE_TIMEOUT_MS`) یا عدم فعالیت بدنه Undici (`FETCH_BODY_TIMEOUT_MS`) متوقف می شود.
-If you run OmniRoute behind Nginx, Caddy, Cloudflare, or another reverse proxy, make sure the proxy
-timeouts are also higher than your OmniRoute stream/fetch timeouts.
+اگر OmniRoute را پشت Nginx، Caddy، Cloudflare یا یک پراکسی معکوس دیگر اجرا میکنید، از پراکسی مطمئن شوید
+زمانبندیها نیز بیشتر از زمانهای زمانی پخش/واکشی OmniRoute شما هستند.
-### 2) Connect providers and create your API key
+### 2) ارائه دهندگان را متصل کنید و کلید API خود را ایجاد کنید
-1. Open Dashboard → `Providers` and connect at least one provider (OAuth or API key).
-2. Open Dashboard → `Endpoints` and create an API key.
-3. (Optional) Open Dashboard → `Combos` and set your fallback chain.
+1. داشبورد → `Providers` را باز کنید و حداقل یک ارائه دهنده (کلید OAuth یا API) را وصل کنید.
+2. داشبورد → `Endpoints` را باز کنید و یک کلید API ایجاد کنید.
+3. (اختیاری) داشبورد → `Combos` را باز کنید و زنجیره بازگشتی خود را تنظیم کنید.
-### 3) Point your coding tool to OmniRoute
+### 3) ابزار کدنویسی خود را روی OmniRoute قرار دهید
```txt
Base URL: http://localhost:20128/v1
@@ -853,20 +853,20 @@ API Key: [copy from Endpoint page]
Model: if/kimi-k2-thinking (or any provider/model prefix)
```
-### 4) Enable and validate protocols (v2.0)
+### 4) فعال کردن و اعتبارسنجی پروتکل ها (v2.0)
-**MCP (for tool-driven operations):**
+**MCP (برای عملیات ابزار محور):**
```bash
omniroute --mcp
```
-Then connect your MCP client over `stdio` and test tools like:
+سپس مشتری MCP خود را به `stdio` متصل کنید و ابزارهایی مانند:
- `omniroute_get_health`
- `omniroute_list_combos`
-**A2A (for agent-to-agent workflows):**
+**A2A (برای گردش کار عامل به عامل):**
```bash
curl http://localhost:20128/.well-known/agent.json
@@ -878,15 +878,15 @@ curl -X POST http://localhost:20128/a2a \
-d '{"jsonrpc":"2.0","id":"quickstart","method":"message/send","params":{"skill":"quota-management","messages":[{"role":"user","content":"Give me a short quota summary."}]}}'
```
-### 5) Validate everything end-to-end (recommended)
+### 5) اعتبارسنجی همه چیز از انتها به انتها (توصیه می شود)
```bash
npm run test:protocols:e2e
```
-This suite validates real MCP and A2A client flows against a running app.
+این مجموعه، جریان های سرویس گیرنده MCP و A2A واقعی را در برابر یک برنامه در حال اجرا تأیید می کند.
-### Alternative: run from source
+### جایگزین: از منبع اجرا شود
```bash
cp .env.example .env
@@ -897,7 +897,7 @@ PORT=20128 DASHBOARD_PORT=20129 NEXT_PUBLIC_BASE_URL=http://localhost:20129 npm
Void Linux (`xbps-src` template)
-For Void Linux users, you can build a native package using `xbps-src`. Save this block as `srcpkgs/omniroute/template`:
+برای کاربران Void Linux، می توانید یک بسته بومی با استفاده از `xbps-src` بسازید. این بلوک را به عنوان `srcpkgs/omniroute/template` ذخیره کنید:
```bash
# Template file for 'omniroute'
@@ -1005,9 +1005,9 @@ post_install() {
## 🐳 Docker
-OmniRoute is available as a public Docker image on [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute).
+OmniRoute به عنوان یک تصویر عمومی Docker در [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute) در دسترس است.
-**Quick run:**
+**اجرای سریع:**
```bash
docker run -d \
@@ -1019,7 +1019,7 @@ docker run -d \
diegosouzapw/omniroute:latest
```
-**With environment file:**
+**به همراه فایل محیطی:**
```bash
# Copy and edit .env first
@@ -1035,7 +1035,7 @@ docker run -d \
diegosouzapw/omniroute:latest
```
-**Using Docker Compose:**
+**با استفاده از Docker نوشتن:**
```bash
# Base profile (no CLI tools)
@@ -1045,22 +1045,22 @@ docker compose --profile base up -d
docker compose --profile cli up -d
```
-Dashboard support for Docker deployments now includes a one-click **Cloudflare Quick Tunnel** on `Dashboard → Endpoints`. The first enable downloads `cloudflared` only when needed, starts a temporary tunnel to your current `/v1` endpoint, and shows the generated `https://*.trycloudflare.com/v1` URL directly below your normal public URL.
+پشتیبانی داشبورد برای استقرار Docker اکنون شامل یک **Cloudflare Quick Tunnel** با یک کلیک روی `Dashboard → Endpoints` است. اولین فعال `cloudflared` را فقط در صورت نیاز دانلود میکند، یک تونل موقت به نقطه پایانی فعلی `/v1` شما راهاندازی میکند و `https://*.trycloudflare.com/v1` URL تولید شده را مستقیماً زیر URL عمومی عادی شما نشان میدهد.
Notes:
-- Quick Tunnel URLs are temporary and change after every restart.
-- Quick Tunnels are not auto-restored after an OmniRoute or container restart. Re-enable them from the dashboard when needed.
-- Managed install currently supports Linux, macOS, and Windows on `x64` / `arm64`.
-- Managed Quick Tunnels default to HTTP/2 transport to avoid noisy QUIC UDP buffer warnings in constrained container environments. Set `CLOUDFLARED_PROTOCOL=quic` or `auto` if you want a different transport.
-- Docker images bundle system CA roots and pass them to managed `cloudflared`, which avoids TLS trust failures when the tunnel bootstraps inside the container.
-- SQLite runs in WAL mode. `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`.
-- The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40` (or similar) so manual stops do not cut off shutdown cleanup.
-- Set `CLOUDFLARED_BIN=/absolute/path/to/cloudflared` if you want OmniRoute to use an existing binary instead of downloading one.
+- URL های تونل سریع موقتی هستند و پس از هر بار راه اندازی مجدد تغییر می کنند.
+- تونل های سریع پس از راه اندازی مجدد OmniRoute یا کانتینر به طور خودکار بازیابی نمی شوند. در صورت نیاز دوباره آنها را از داشبورد فعال کنید.
+- نصب مدیریت شده در حال حاضر از لینوکس، macOS و ویندوز در `x64` / `arm64` پشتیبانی می کند.
+- تونل های سریع مدیریت شده به طور پیش فرض برای حمل و نقل HTTP/2 برای جلوگیری از هشدارهای بافر QUIC UDP پر سر و صدا در محیط های کانتینری محدود. اگر می خواهید حمل و نقل متفاوتی داشته باشید، `CLOUDFLARED_PROTOCOL=quic` یا `auto` را تنظیم کنید.
+- تصاویر Docker ریشه های CA سیستم را بسته بندی می کند و آنها را به `cloudflared` مدیریت شده ارسال می کند، که از خرابی اعتماد TLS در هنگام بوت استرپ تونل در داخل ظرف جلوگیری می کند.
+- SQLite در حالت WAL اجرا می شود. باید به `docker stop` اجازه داده شود تا پایان یابد تا OmniRoute بتواند آخرین تغییرات را به `storage.sqlite` بازرسی کند.
+- فایل های Compose همراه از قبل یک دوره مهلت توقف 40 ثانیه تعیین کرده اند. اگر مستقیماً تصویر را اجرا میکنید، `--stop-timeout 40` (یا مشابه) را نگه دارید تا توقفهای دستی پاکسازی خاموش را قطع نکنند.
+- اگر می خواهید OmniRoute از یک باینری موجود به جای دانلود استفاده کند، `CLOUDFLARED_BIN=/absolute/path/to/cloudflared` را تنظیم کنید.
-**Using Docker Compose with Caddy (HTTPS Auto-TLS):**
+**استفاده از Docker Compose with Caddy (HTTPS Auto-TLS):**
-OmniRoute can be securely exposed using Caddy's automatic SSL provisioning. Ensure your domain's DNS A record points to your server's IP.
+OmniRoute را می توان با استفاده از تهیه خودکار SSL Caddy به طور ایمن در معرض دید قرار داد. مطمئن شوید که رکورد DNS A دامنه شما به IP سرور شما اشاره دارد.
```yaml
services:
@@ -1087,26 +1087,26 @@ volumes:
omniroute-data:
```
-| Image | Tag | Size | Description |
+| تصویر | برچسب | اندازه | توضیحات |
| ------------------------ | -------- | ------ | --------------------- |
-| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release |
-| `diegosouzapw/omniroute` | `3.6.2` | ~250MB | Current version |
+| `diegosouzapw/omniroute` | `latest` | ~250 مگابایت | آخرین نسخه پایدار |
+| `diegosouzapw/omniroute` | `3.6.2` | ~250 مگابایت | نسخه فعلی |
---
-## 🖥️ Desktop App — Offline & Always-On
+## 🖥️ برنامه دسکتاپ — آفلاین و همیشه روشن
-> 🆕 **NEW!** OmniRoute is now available as a **native desktop application** for Windows, macOS, and Linux.
+> 🆕 **جدید!** OmniRoute اکنون به عنوان یک **برنامه دسکتاپ بومی** برای Windows، macOS و Linux در دسترس است.
-Run OmniRoute as a standalone desktop app — no terminal, no browser, no internet required for local models. The Electron-based app includes:
+OmniRoute را به عنوان یک برنامه دسکتاپ مستقل اجرا کنید - بدون پایانه، بدون مرورگر، بدون نیاز به اینترنت برای مدلهای محلی. برنامه مبتنی بر الکترون شامل موارد زیر است:
-- 🖥️ **Native Window** — Dedicated app window with system tray integration
-- 🔄 **Auto-Start** — Launch OmniRoute on system login
-- 🔔 **Native Notifications** — Get alerts for quota exhaustion or provider issues
-- ⚡ **One-Click Install** — NSIS (Windows), DMG (macOS), AppImage (Linux)
-- 🌐 **Offline Mode** — Works fully offline with bundled server
+- 🖥️ **پنجره بومی** - پنجره برنامه اختصاصی با ادغام سینی سیستم
+- 🔄 **شروع خودکار** — راه اندازی OmniRoute در ورود به سیستم
+- 🔔 ** اعلان های بومی ** - هشدارهایی را برای اتمام سهمیه یا مشکلات ارائه دهنده دریافت کنید
+- ⚡ **نصب با یک کلیک** - NSIS (ویندوز)، DMG (macOS)، AppImage (لینوکس)
+- 🌐 ** حالت آفلاین ** - با سرور همراه به طور کامل آفلاین کار می کند
-### Inicio Rápido
+### اینیسیو راپیدو
```bash
# Development mode
@@ -1119,50 +1119,50 @@ npm run electron:build:mac # macOS (.dmg) — x64 & arm64
npm run electron:build:linux # Linux (.AppImage)
```
-### System Tray
+### سینی سیستم
-When minimized, OmniRoute lives in your system tray with quick actions:
+هنگامی که به حداقل می رسد، OmniRoute با اقدامات سریع در سینی سیستم شما زندگی می کند:
-- Open dashboard
-- Change server port
-- Quit application
+- داشبورد را باز کنید
+- تغییر پورت سرور
+- برنامه را ترک کنید
-📖 Full documentation: [`electron/README.md`](electron/README.md)
+📖 مستندات کامل: [`electron/README.md`](electron/README.md)
---
-## 💰 Pricing at a Glance
+## 💰 قیمت در یک نگاه
-| Tier | Provider | Cost | Quota Reset | Best For |
+| ردیف | ارائه دهنده | هزینه | بازنشانی سهمیه | بهترین برای |
| ------------------- | --------------------------- | ------------------------------------- | --------------------- | ---------------------------------- |
-| **💳 SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed |
-| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users |
-| | GitHub Copilot | $10-19/mo | Monthly | GitHub users |
-| **🔑 API KEY** | NVIDIA NIM | **FREE ACCESS** (current terms apply) | ~40 RPM | 70+ open models |
-| | Cerebras | **FREE** (1M tok/day) | 60K TPM / 30 RPM | World's fastest |
-| | Groq | **FREE** (30 RPM) | 14.4K RPD | Ultra-fast Llama/Gemma |
-| | DeepSeek V3.2 | $0.27/$1.10 per 1M | None | Best price/quality reasoning |
-| | xAI Grok-4 Fast | **$0.20/$0.50 per 1M** 🆕 | None | Fastest + tool calling, ultralow |
-| | xAI Grok-4 (standard) | $0.20/$1.50 per 1M 🆕 | None | Reasoning flagship from xAI |
-| | Mistral | Free trial + paid | Rate limited | European AI |
-| | OpenRouter | Pay-per-use | None | 100+ models aggr. |
-| **💰 CHEAP** | GLM-5 (via Z.AI) 🆕 | $0.5/1M | Daily 10AM | 128K output, newest flagship |
-| | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup |
-| | MiniMax M2.5 🆕 | $0.3/1M input | 5-hour rolling | Reasoning + agentic tasks |
-| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option |
-| | Kimi K2.5 (Moonshot API) 🆕 | Pay-per-use | None | Direct Moonshot API access |
-| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost |
-| **🆓 FREE ACCESS** | Qoder | **$0** | Limits apply | Selected models; terms apply |
-| | Qwen | **$0** | Limits apply | Selected models; terms apply |
-| | Kiro | **$0** | Credit/account limits | Claude access; current terms apply |
-| | LongCat signup credit | **$0** (10M one-time; KYC) | One-time | Signup grant; not recurring |
-| | Pollinations AI 🆕 | **$0** (no key needed) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama 4 |
-| | Cloudflare Workers AI 🆕 | **$0** (10K Neurons/day) | ~150 resp/day | 50+ models, global edge |
-| | Scaleway AI 🆕 | **$0** (1M tokens total) | Rate limited | EU/GDPR, Qwen3 235B, Llama 70B |
-
-> 🆕 **New models added (Mar 2026):** Grok-4 Fast family at $0.20/$0.50/M (benchmarked at 1143ms — 30% faster than Gemini 2.5 Flash), GLM-5 via Z.AI with 128K output, MiniMax M2.5 reasoning, DeepSeek V3.2 updated pricing, Kimi K2.5 via Moonshot direct API.
-
-**💡 $0 Combo Stack — The Complete Free Setup:**
+| **💳 اشتراک ** | کد Claude (Pro) | 20 دلار در ماه | 5 ساعت + هفتگی | قبلاً مشترک شده است |
+| | Codex (Plus/Pro) | 20-200 دلار در ماه | 5 ساعت + هفتگی | کاربران OpenAI |
+| | GitHub Copilot | 10-19 دلار در ماه | ماهانه | کاربران GitHub |
+| **🔑 کلید API ** | NVIDIA NIM | **دسترسی رایگان** (شرایط فعلی اعمال می شود) | ~40 دور در دقیقه | 70+ مدل باز |
+| | Cerebras | **رایگان** (1 میلیون توک در روز) | 60K TPM / 30 RPM | سریعترین جهان |
+| | Groq | **رایگان** (30 دور در دقیقه) | 14.4K RPD | Llama/Gemma فوق العاده سریع |
+| | DeepSeek V3.2 | 0.27 دلار / 1.10 دلار در هر 1 میلیون | هیچکدام | بهترین استدلال قیمت/کیفیت |
+| | xAI Grok-4 Fast | **0.20$/0.50$ در هر 1 میلیون** 🆕 | هیچکدام | سریعترین + فراخوانی ابزار، فوق العاده کم |
+| | xAI Grok-4 (استاندارد) | 0.20 دلار / 1.50 دلار به ازای هر 1 میلیون 🆕 | هیچکدام | گل سرسبد استدلال از xAI |
+| | میسترال | آزمایشی رایگان + پولی | نرخ محدود | هوش مصنوعی اروپایی |
+| | OpenRouter | پرداخت به ازای استفاده | هیچکدام | 100+ مدل aggr. |
+| **💰 ارزان ** | GLM-5 (از طریق Z.AI) 🆕 | 0.5/1 میلیون دلار | روزانه 10 صبح | خروجی 128K، جدیدترین پرچمدار |
+| | GLM-4.7 | 0.6/1 میلیون دلار | روزانه 10 صبح | پشتیبان بودجه |
+| | MiniMax M2.5 🆕 | 0.3/1 میلیون دلار ورودی | نورد 5 ساعته | استدلال + وظایف نمایندگی |
+| | MiniMax M2.1 | 0.2/1 میلیون دلار | نورد 5 ساعته | ارزان ترین گزینه |
+| | Kimi K2.5 (Moonshot API) 🆕 | پرداخت به ازای استفاده | هیچکدام | دسترسی مستقیم Moonshot API |
+| | Kimi K2 | 9 دلار در ماه آپارتمان | 10 میلیون توکن در ماه | هزینه قابل پیش بینی |
+| **🆓 دسترسی رایگان ** | Qoder | **0$** | محدودیت اعمال می شود | مدل های منتخب؛ شرایط اعمال می شود |
+| | Qwen | **0$** | محدودیت اعمال می شود | مدل های منتخب؛ شرایط اعمال می شود |
+| | Kiro | **0$** | محدودیت اعتبار/حساب | دسترسی Claude؛ شرایط فعلی اعمال می شود |
+| | اعتبار ثبت نام LongCat | **0$** (10 میلیون یکبار مصرف؛ KYC) | یکبار مصرف | کمک هزینه ثبت نام؛ تکرار نشدن |
+| | Pollinations AI 🆕 | **0$** (بدون نیاز به کلید) | 1 req/15s | GPT-5، Claude، DeepSeek، Llama 4 |
+| | Cloudflare Workers AI 🆕 | **0$** (10 هزار نورون در روز) | ~150 بار در روز | 50+ مدل، لبه جهانی |
+| | Scaleway AI 🆕 | **0$** (مجموع 1 میلیون توکن) | نرخ محدود | EU/GDPR، Qwen3 235B، Llama 70B |
+
+> 🆕 **مدلهای جدید اضافه شده (مارس 2026):** خانواده Grok-4 Fast با 0.20 دلار/0.50 دلار در میلیون دلار (معیار شده در 1143 میلیثانیه — 30 درصد سریعتر از فلش Gemini 2.5)، GLM-5 از طریق Z.AI، خروجی Z.AI، Z.AI، Z. DeepSeek V3.2 قیمت به روز شده، Kimi K2.5 از طریق Moonshot مستقیم API.
+
+**💡 $0 Combo Stack — راه اندازی کامل رایگان:**
```
# 🆓 Free-access examples — provider limits and terms apply
@@ -1179,138 +1179,138 @@ NVIDIA NIM (nvidia/) → selected models — current rate limits apply
Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
```
-**Current $0 access where listed; availability is not guaranteed.** A combo can try the next eligible route when a quota or upstream fails.
+**دسترسی کنونی $0 در جایی که فهرست شده است. در دسترس بودن تضمین نشده است.** یک ترکیبی میتواند مسیر واجد شرایط بعدی را در صورت شکست سهمیه یا بالادستی امتحان کند.
---
---
-## 🆓 Free Models — What You Actually Get
+## 🆓 مدل های رایگان - آنچه در واقع به دست می آورید
-> The entries below summarize access that was listed as free when audited. Provider quotas, card/account/KYC requirements, models, regions and terms can change. A combo broadens fallback coverage but does not guarantee uninterrupted $0 access.
+> ورودیهای زیر دسترسیهایی را که هنگام ممیزی رایگان فهرست شدهاند، خلاصه میکنند. سهمیههای ارائهدهنده، کارت/حساب/ الزامات KYC، مدلها، مناطق و شرایط میتوانند تغییر کنند. ترکیبی پوشش بازگشتی را گسترش می دهد اما دسترسی بدون وقفه $0 را تضمین نمی کند.
-### 🔵 CLAUDE MODELS (via Kiro — AWS Builder ID)
+### 🔵 CLAUDE MODELS (از طریق Kiro — AWS Builder ID)
-| Model | Prefix | Limit | Rate Limit |
+| مدل | پیشوند | محدود | محدودیت نرخ |
| ------------------- | ------ | ------------- | --------------------- |
-| `claude-sonnet-4.5` | `kr/` | No published token cap | Provider/account limits may apply |
-| `claude-haiku-4.5` | `kr/` | No published token cap | Provider/account limits may apply |
-| `claude-opus-4.6` | `kr/` | No published token cap | Latest Opus; provider/account limits apply |
+| `claude-sonnet-4.5` | `kr/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `claude-haiku-4.5` | `kr/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `claude-opus-4.6` | `kr/` | بدون درپوش توکن منتشر شده | آخرین Opus; محدودیت های ارائه دهنده/حساب اعمال می شود |
-### 🟢 QODER MODELS (Free PAT via qodercli)
+### مدل QODER (رایگان PAT از طریق qodercli)
-| Model | Prefix | Limit | Rate Limit |
+| مدل | پیشوند | محدود | محدودیت نرخ |
| ------------------ | ------ | ------------- | --------------- |
-| `kimi-k2-thinking` | `if/` | No published token cap | Provider/account limits may apply |
-| `qwen3-coder-plus` | `if/` | No published token cap | Provider/account limits may apply |
-| `deepseek-r1` | `if/` | No published token cap | Provider/account limits may apply |
-| `minimax-m2.1` | `if/` | No published token cap | Provider/account limits may apply |
-| `kimi-k2` | `if/` | No published token cap | Provider/account limits may apply |
+| `kimi-k2-thinking` | `if/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `qwen3-coder-plus` | `if/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `deepseek-r1` | `if/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `minimax-m2.1` | `if/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `kimi-k2` | `if/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
-> Recommended connection method: **Personal Access Token + `qodercli`**. Browser OAuth is
-> experimental and disabled by default unless `QODER_OAUTH_*` environment variables are configured.
+> روش اتصال پیشنهادی: ** رمز دسترسی شخصی + `qodercli`**. مرورگر OAuth است
+> آزمایشی و به طور پیش فرض غیرفعال است مگر اینکه متغیرهای محیطی `QODER_OAUTH_*` پیکربندی شده باشند.
-### 🟡 QWEN MODELS (Device Code Auth)
+### 🡑 مدلهای QWEN (تأیید کد دستگاه)
-| Model | Prefix | Limit | Rate Limit |
+| مدل | پیشوند | محدود | محدودیت نرخ |
| ------------------- | ------ | ------------- | ------------------- |
-| `qwen3-coder-plus` | `qw/` | No published token cap | Provider/account limits may apply |
-| `qwen3-coder-flash` | `qw/` | No published token cap | Provider/account limits may apply |
-| `qwen3-coder-next` | `qw/` | No published token cap | Provider/account limits may apply |
-| `vision-model` | `qw/` | No published token cap | Multimodal; provider/account limits may apply |
+| `qwen3-coder-plus` | `qw/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `qwen3-coder-flash` | `qw/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `qwen3-coder-next` | `qw/` | بدون درپوش توکن منتشر شده | ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
+| `vision-model` | `qw/` | بدون درپوش توکن منتشر شده | چند وجهی؛ ممکن است محدودیت های ارائه دهنده/حساب اعمال شود |
-### ⚫ NVIDIA NIM (Free API Key — build.nvidia.com)
+### ⚫ NVIDIA NIM (کلید رایگان API — build.nvidia.com)
-| Tier | Daily Limit | Rate Limit | Notes |
+| ردیف | محدودیت روزانه | محدودیت نرخ | یادداشت ها |
| ---------- | ------------ | ----------- | ------------------------------------------------------ |
-| Free (Dev) | No token cap | **~40 RPM** | 70+ models; transitioning to pure rate limits mid-2025 |
+| رایگان (Dev) | بدون درپوش رمزی | **~40 دور در دقیقه** | 70+ مدل؛ انتقال به محدودیت های نرخ خالص اواسط سال 2025 |
-Popular free models: `moonshotai/kimi-k2.5` (Kimi K2.5), `z-ai/glm4.7` (GLM 4.7), `deepseek-ai/deepseek-v3.2` (DeepSeek V3.2), `nvidia/llama-3.3-70b-instruct`, `deepseek/deepseek-r1`
+مدلهای رایگان پرطرفدار: `moonshotai/kimi-k2.5` (Kimi K2.5)، `z-ai/glm4.7` (GLM 4.7)، `deepseek-ai/deepseek-v3.2` (DeepSeek V3.2)، KimiTXQ3.
-### ⚪ CEREBRAS (Free API Key — inference.cerebras.ai)
+### ⚪ CEREBRAS (کلید رایگان API — inference.cerebras.ai)
-| Tier | Daily Limit | Rate Limit | Notes |
+| ردیف | محدودیت روزانه | محدودیت نرخ | یادداشت ها |
| ---- | ----------------- | ---------------- | ------------------------------------------- |
-| Free | **1M tokens/day** | 60K TPM / 30 RPM | World's fastest LLM inference; resets daily |
+| رایگان | **1 میلیون توکن در روز** | 60K TPM / 30 RPM | سریعترین استنتاج LLM در جهان. تنظیم مجدد روزانه |
-Available free: `llama-3.3-70b`, `llama-3.1-8b`, `deepseek-r1-distill-llama-70b`
+رایگان موجود: `llama-3.3-70b`، `llama-3.1-8b`، `deepseek-r1-distill-llama-70b`
-### 🔴 GROQ (Free API Key — console.groq.com)
+### 🔴 GROQ (کلید رایگان API — console.groq.com)
-| Tier | Daily Limit | Rate Limit | Notes |
+| ردیف | محدودیت روزانه | محدودیت نرخ | یادداشت ها |
| ---- | ------------- | ---------------- | ----------------------------------------- |
-| Free | **14.4K RPD** | 30 RPM per model | No credit card; 429 on limit, not charged |
+| رایگان | **14.4K RPD** | 30 دور در دقیقه در هر مدل | بدون کارت اعتباری؛ 429 محدود، شارژ نشده |
-Available free: `llama-3.3-70b-versatile`, `gemma2-9b-it`, `mixtral-8x7b`, `whisper-large-v3`
+رایگان موجود: `llama-3.3-70b-versatile`، `gemma2-9b-it`، `mixtral-8x7b`، `whisper-large-v3`
-### 🔴 LONGCAT AI (Signup credit — KYC required)
+### 🔴 LONGCAT AI (اعتبار ثبت نام — KYC مورد نیاز است)
-| Model | Prefix | Current catalog grant | Notes |
+| مدل | پیشوند | کمک هزینه کاتالوگ فعلی | یادداشت ها |
| ------------- | ------ | ----------------------- | --------------------------------------------------- |
-| `LongCat-2.0` | `lc/` | **10M tokens one-time** | Signup grant; not a recurring monthly or daily pool |
+| `LongCat-2.0` | `lc/` | **10 میلیون توکن یکبار مصرف** | کمک هزینه ثبت نام؛ نه یک استخر تکراری ماهانه یا روزانه |
-> Provider terms, eligibility and model availability can change. See [`FREE_TIERS.md`](../../reference/FREE_TIERS.md) for the audited catalog entry.
+> شرایط ارائه دهنده، واجد شرایط بودن و در دسترس بودن مدل می تواند تغییر کند. [`FREE_TIERS.md`](../../reference/FREE_TIERS.md) را برای ورودی کاتالوگ ممیزی شده ببینید.
-### 🟢 POLLINATIONS AI (No API Key Required) 🆕
+### AI POLLINATIONS (بدون نیاز به کلید API) 🆕
-| Model | Prefix | Rate Limit | Provider Behind |
+| مدل | پیشوند | محدودیت نرخ | ارائه دهنده پشت |
| ---------- | ------ | ---------- | ------------------ |
-| `openai` | `pol/` | 1 req/15s | GPT-5 |
-| `claude` | `pol/` | 1 req/15s | Anthropic Claude |
-| `gemini` | `pol/` | 1 req/15s | Google Gemini |
-| `deepseek` | `pol/` | 1 req/15s | DeepSeek V3 |
-| `llama` | `pol/` | 1 req/15s | Meta Llama 4 Scout |
-| `mistral` | `pol/` | 1 req/15s | Mistral AI |
+| `openai` | `pol/` | 1 req/15s | GPT-5 |
+| `claude` | `pol/` | 1 req/15s | آنتروپیک Claude |
+| `gemini` | `pol/` | 1 req/15s | گوگل جمینی |
+| `deepseek` | `pol/` | 1 req/15s | DeepSeek V3 |
+| `llama` | `pol/` | 1 req/15s | Meta Llama 4 Scout |
+| `mistral` | `pol/` | 1 req/15s | Mistral AI |
-> ✨ **Zero friction:** No signup, no API key. Add the Pollinations provider with an empty key field and it works immediately.
+> ✨ **اصطکاک صفر:** بدون ثبت نام، بدون کلید API. ارائه دهنده Pollinations را با یک فیلد کلید خالی اضافه کنید و بلافاصله کار می کند.
-### 🟠 CLOUDFLARE WORKERS AI (Free API Key — cloudflare.com) 🆕
+### CLOUDFLARE WORKERS AI (کلید رایگان API — cloudflare.com) 🆕
-| Tier | Daily Neurons | Equivalent Usage | Notes |
+| ردیف | نورون های روزانه | استفاده معادل | یادداشت ها |
| ---- | ------------- | --------------------------------------- | ----------------------- |
-| Free | **10,000** | ~150 LLM resp / 500s audio / 15K embeds | Global edge, 50+ models |
+| رایگان | **10000** | ~150 LLM resp / صدای 500s / 15K جاسازی | لبه جهانی، 50+ مدل |
-Popular free models: `@cf/meta/llama-3.3-70b-instruct`, `@cf/google/gemma-3-12b-it`, `@cf/openai/whisper-large-v3-turbo` (free audio!), `@cf/qwen/qwen2.5-coder-15b-instruct`
+مدل های رایگان محبوب: `@cf/meta/llama-3.3-70b-instruct`، `@cf/google/gemma-3-12b-it`، `@cf/openai/whisper-large-v3-turbo` (صدای رایگان!)، `@cf/qwen/qwen2.5-coder-15b-instruct`
-> Requires API Token + Account ID from [dash.cloudflare.com](https://dash.cloudflare.com). Store Account ID in provider settings.
+> به رمز API + شناسه حساب از [dash.cloudflare.com](https://dash.cloudflare.com) نیاز دارد. شناسه حساب را در تنظیمات ارائه دهنده ذخیره کنید.
-### 🟣 SCALEWAY AI (1M Free Tokens — scaleway.com) 🆕
+### SCALEWAY AI (1 میلیون توکن رایگان — scaleway.com) 🆕
-| Tier | Free Quota | Location | Notes |
+| ردیف | سهمیه آزاد | مکان | یادداشت ها |
| ---- | ------------- | ------------ | ----------------------------------- |
-| Free | **1M tokens** | 🇫🇷 Paris, EU | No credit card needed within limits |
+| رایگان | **1 میلیون توکن** | 🇫🇷 پاریس، اتحادیه اروپا | بدون نیاز به کارت اعتباری در محدوده |
-Available free: `qwen3-235b-a22b-instruct-2507` (Qwen3 235B!), `llama-3.1-70b-instruct`, `mistral-small-3.2-24b-instruct-2506`, `deepseek-v3-0324`
+رایگان موجود: `qwen3-235b-a22b-instruct-2507` (Qwen3 235B!)، `llama-3.1-70b-instruct`، `mistral-small-3.2-24b-instruct-2506`، `deepseek-v3-0324`
-> EU/GDPR compliant. Get API key at [console.scaleway.com](https://console.scaleway.com).
+> مطابق با اتحادیه اروپا/GDPR. کلید API را در [console.scaleway.com](https://console.scaleway.com) دریافت کنید.
-> **💡 Free-access examples (provider limits and terms apply):**
+> **💡 نمونه های دسترسی رایگان (محدودیت ها و شرایط ارائه دهنده اعمال می شود):**
>
> ```
-> Kiro (kr/) → Claude access — account/credit limits apply
-> Qoder (if/) → selected models — no published token cap; limits apply
-> LongCat (lc/) → LongCat-2.0 — 10M one-time signup credit; KYC required
-> Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
-> Qwen (qw/) → selected models — no published token cap; limits apply
-> Gemini (gemini/) → selected free-tier models — current quotas apply
-> Cloudflare AI (cf/) → 50+ models — 10K Neurons/day
-> Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
-> Groq (groq/) → selected models — current per-model rate limits apply
-> NVIDIA NIM (nvidia/) → selected models — current rate limits apply
-> Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
+> Kiro (kr/) → دسترسی Claude — محدودیت های حساب/اعتبار اعمال می شود
+> Qoder (اگر/) ← مدل های انتخابی — بدون پوشش توکن منتشر شده. محدودیت اعمال می شود
+> LongCat (lc/) → LongCat-2.0 — 10 میلیون اعتبار ثبت نام یکباره؛ KYC مورد نیاز است
+> Pollinations (pol/) → GPT-5، Claude، DeepSeek، Llama 4 — بدون نیاز به کلید
+> Qwen (qw/) ← مدلهای انتخابی — بدون درپوش توکن منتشر شده. محدودیت اعمال می شود
+> جمینی (جمینی/) ← مدل های سطح آزاد انتخاب شده — سهمیه های فعلی اعمال می شود
+> Cloudflare AI (cf/) → بیش از 50 مدل — 10 هزار نورون در روز
+> Scaleway (scw/) → Qwen3 235B، Llama 70B — 1 میلیون توکن رایگان (EU)
+> Groq (groq/) ← مدلهای انتخابی - محدودیتهای نرخ فعلی برای هر مدل اعمال میشود
+> NVIDIA NIM (nvidia/) → مدل های انتخابی — محدودیت های نرخ فعلی اعمال می شود
+> Cerebras (مغز/) → لاما/Qwen سریعترین جهان — 1 میلیون توک در روز
> ```
-## 🎙️ Free Transcription Combo
+## 🎙️ ترکیب رونویسی رایگان
-> Transcription access depends on each upstream allowance — Deepgram and AssemblyAI signup credits can lead, with Groq Whisper as a rate-limited fallback.
+> دسترسی به رونویسی به هر کمک هزینه بالادستی بستگی دارد - اعتبارات ثبت نام Deepgram و AssemblyAI می توانند منجر شوند، با Groq Whisper به عنوان یک بازگشت با نرخ محدود.
-| Provider | Free Credits | Best Model | Rate Limit |
+| ارائه دهنده | اعتبار رایگان | بهترین مدل | محدودیت نرخ |
| ----------------- | --------------------------- | -------------------------------------------- | ---------------------------------------- |
-| 🟢 **Deepgram** | **$200 free** (signup) | `nova-3` — best accuracy, 30+ languages | No RPM limit on free credits |
-| 🔵 **AssemblyAI** | **$50 free** (signup) | `universal-3-pro` — chapters, sentiment, PII | No RPM limit on free credits |
-| 🔴 **Groq** | **Free tier; limits apply** | `whisper-large-v3` — OpenAI Whisper | Current model-specific rate limits apply |
+| 🢢 **دیپگرام** | **200 دلار رایگان** (ثبت نام) | `nova-3` — بهترین دقت، بیش از 30 زبان | بدون محدودیت RPM در اعتبارات رایگان |
+| 🔵 **AssemblyAI** | **50 دلار رایگان ** (ثبت نام) | `universal-3-pro` — فصول، احساسات، PII | بدون محدودیت RPM در اعتبارات رایگان |
+| 🔴 **Groq** | ** ردیف آزاد؛ محدودیت اعمال می شود** | `whisper-large-v3` — OpenAI Whisper | محدودیتهای نرخ ویژه مدل فعلی اعمال میشود |
-**Suggested combo in `/dashboard/combos`:**
+** ترکیب پیشنهادی در `/dashboard/combos`:**
```
Name: free-transcription
@@ -1321,180 +1321,180 @@ Nodes:
[3] groq/whisper-large-v3 → free access; rate limits apply
```
-Then in `/dashboard/media` → **Transcription** tab: upload any audio or video file → select your combo endpoint → get transcription in supported formats.
+سپس در زبانه `/dashboard/media` → **رونویسی**: هر فایل صوتی یا تصویری را آپلود کنید → نقطه پایانی ترکیبی خود را انتخاب کنید → رونویسی را در قالب های پشتیبانی شده دریافت کنید.
-## 💡 Key Features
+## 💡 ویژگی های کلیدی
-OmniRoute v3.6 is built as an operational platform, not just a relay proxy.
+OmniRoute v3.6 به عنوان یک پلت فرم عملیاتی ساخته شده است، نه فقط یک پروکسی رله.
-### 🆕 New — v3.6.x Highlights (Apr 2026)
+### 🆕 جدید — نسخه های برجسته نسخه 3.6.x (آوریل 2026)
-| Feature | What It Does |
+| ویژگی | چه می کند |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 🌐 **V1 WebSocket Bridge** | OpenAI-compatible WebSocket traffic upgraded and proxied via `/v1/ws` — full streaming over WS with session auth (API key or session cookie) |
-| 🔑 **Sync Tokens & Config Bundle** | Issue/revoke sync tokens for config sync endpoints. Config bundles versioned with ETag for bandwidth-efficient polling |
-| 🧠 **GLM Thinking (glmt) Preset** | GLM Thinking registered first-class: 65 536 max tokens, 24 576 thinking budget, 900s timeout, usage sync & pricing — Claude-compatible API |
-| 🔢 **Hybrid Token Counting** | Uses provider-side `/messages/count_tokens` when available; falls back to estimation — accurate usage tracking without guessing |
-| 🌱 **Model Alias Auto-Seed** | 30+ cross-proxy dialect aliases normalised at startup — no more routing mismatches |
-| 🛡️ **Safe Outbound Fetch** | All provider validation and model discovery go through a guarded fetch layer blocking private/local URLs with retry, timeout, and SSRF protection |
-| ⏳ **Wait For Cooldown** | Server-side chat retries when every candidate connection is cooling down; configurable `enabled`, `maxRetries`, and `maxRetryWaitSec` |
-| 🔍 **Runtime Env Validation** | Startup validates all env vars with Zod schemas — clear errors for missing secrets, invalid URLs, or wrong types |
-| 📋 **Compliance Audit Expansion** | Structured audit logs with pagination, request context, auth events, provider CRUD events, and SSRF-blocked validation logging |
-| 🔐 **TPS Log Metric** | Log details modal shows Tokens Per Second (TPS) — quick performance at-a-glance for every request |
-| 🗑️ **Uninstall / Full Uninstall** | `npm run uninstall` keeps data, `npm run uninstall:full` removes everything — clean removal for all install methods |
-| 🔧 **OAuth Env Repair** | One-click "Repair env" action for OAuth providers restores missing env vars and fixes broken auth state |
-| 🔒 **Graceful Electron Shutdown** | Electron `before-quit` shuts down Next.js gracefully, preventing SQLite WAL database locks on desktop close |
-| 👁️ **Model Visibility Toggle** | Per-model visibility toggle (👁 icon) with search filter and active-count badge (`N/M active`) on provider pages |
-| 📧 **Email Privacy Masking** | OAuth account emails masked (`di*****@g****.com`), full address visible on hover |
-| 🔗 **Context Relay Strategy** | Combo strategy preserving session continuity via structured handoff summaries when accounts rotate mid-conversation |
-| 🛡️ **Proxy Hardening** | Token health check, API key validation, and undici dispatcher all honor proxy config |
-| ⚠️ **Node.js 24 Login Warning** | Login page proactively detects incompatible Node.js versions and shows a clear warning banner |
-| 📎 **Gemini PDF Attachments** | PDF attachments correctly routed to Gemini via `inline_data` and generic base64 detection |
-| 🔒 **CodeQL Security Hardening** | Resolved SSRF, insecure randomness, polynomial ReDoS, and incomplete URL sanitization alerts |
-
-### 🆕 New — ClawRouter-Inspired Improvements (Mar 2026)
-
-| Feature | What It Does |
+| 🌐 ** پل V1 WebSocket ** | ترافیک OpenAI سازگار با WebSocket ارتقا یافته و از طریق `/v1/ws` پروکسی شده است — پخش جریانی کامل از طریق WS با تأیید جلسه (کلید API یا کوکی جلسه) |
+| 🔑 **همگام سازی نشانه ها و بسته پیکربندی** | برای نقاط پایانی همگام سازی پیکربندی، نشانه های همگام سازی را صادر/لغو کنید. بستههای پیکربندی با ETag برای نظرسنجی با پهنای باند کارآمد نسخهبندی شدهاند |
+| 🧠 **GLM از پیش تنظیم تفکر (glmt)** | GLM Thinking ثبت شده درجه یک: 65 536 حداکثر توکن، 24 576 بودجه فکری، مهلت زمانی 900s، همگام سازی استفاده و قیمت گذاری — Claude سازگار API |
+| 🔢 **شمارش توکن هیبریدی** | در صورت موجود بودن از `/messages/count_tokens` سمت ارائه دهنده استفاده می کند. بازگشت به تخمین - ردیابی دقیق استفاده بدون حدس زدن |
+| 🌱 **مدل مستعار خودکار بذر ** | بیش از 30 نام مستعار گویش متقابل پروکسی در هنگام راهاندازی عادی شدند — دیگر عدم تطابق مسیریابی |
+| 🛡️ **واکشی ایمن خروجی** | تمام اعتبار سنجی ارائه دهنده و کشف مدل از طریق یک لایه واکشی محافظت شده انجام می شود که URL های خصوصی/محلی را با تلاش مجدد، مهلت زمانی و حفاظت SSRF مسدود می کند |
+| ⏳ **منتظر خنک شدن باشید** | هنگامی که هر اتصال نامزد در حال خنک شدن است، چت سمت سرور دوباره تکرار می شود. قابل تنظیم `enabled`، `maxRetries`، و `maxRetryWaitSec` |
+| 🔍 **Runtime Env Validation** | راهاندازی تمام env vars را با طرحوارههای Zod تأیید میکند - خطاهای پاک برای اسرار گمشده، URLهای نامعتبر یا انواع اشتباه |
+| 📋 **توسعه حسابرسی انطباق** | گزارشهای حسابرسی ساختاریافته با صفحهبندی، زمینه درخواست، رویدادهای احراز هویت، رویدادهای CRUD ارائهدهنده، و ثبت اعتبارسنجی مسدود شده با SSRF |
+| 🔐 **TPS Log Metric** | جزئیات گزارش مودال نشان می دهد نشانه ها در هر ثانیه (TPS) — عملکرد سریع در یک نگاه برای هر درخواست |
+| 🗑️ **حذف نصب / حذف کامل ** | `npm run uninstall` داده ها را نگه می دارد، `npm run uninstall:full` همه چیز را حذف می کند — حذف تمیز برای همه روش های نصب |
+| 🔧 **OAuth Env Repair** | اقدام "Repair env" با یک کلیک برای ارائه دهندگان OAuth، vars env گم شده را بازیابی می کند و وضعیت احراز هویت شکسته را رفع می کند |
+| 🔒 **خاموشی برازنده الکترون** | Electron `before-quit` Next.js را با زیبایی خاموش می کند و از قفل شدن پایگاه داده SQLite WAL در دسکتاپ جلوگیری می کند |
+| 👁️ **تغییر قابلیت مشاهده مدل** | جابجایی نمای هر مدل (نماد 👁) با فیلتر جستجو و نشان شمارش فعال (`N/M active`) در صفحات ارائه دهنده |
+| 📧 **پوشش حریم خصوصی ایمیل** | ایمیل های حساب OAuth پوشانده شده است (`di*****@g****.com`)، آدرس کامل قابل مشاهده در شناور |
+| 🔗 **استراتژی رله زمینه** | استراتژی ترکیبی حفظ تداوم جلسه از طریق خلاصههای دستیابی ساختاریافته هنگامی که حسابها در میانه مکالمه میچرخند |
+| 🛡️ **سخت شدن پروکسی** | بررسی سلامت توکن، اعتبار سنجی کلید API و undici dispatcher تمام پیکربندی پروکسی افتخاری |
+| ⚠️ **اخطار ورود به سیستم Node.js 24** | صفحه ورود به طور فعال نسخه های ناسازگار Node.js را شناسایی می کند و یک بنر هشدار واضح را نشان می دهد |
+| 📎 **ضمیمه های PDF Gemini** | پیوستهای پیدیاف بهدرستی به Gemini از طریق `inline_data` و تشخیص عمومی base64 هدایت شدند |
+| 🔒 **CodeQL Security Hardening** | SSRF حلشده، تصادفی ناامن، چندجملهای ReDoS و هشدارهای بهداشتی ناقص URL |
+
+### 🆕 جدید - بهبودهای الهام گرفته از ClawRouter (مارس 2026)
+
+| ویژگی | چه می کند |
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
-| ⚡ **Grok-4 Fast Family** | xAI models at $0.20/$0.50/M — benchmarked 1143ms (30% faster than Gemini 2.5 Flash) |
-| 🧠 **GLM-5 via Z.AI** | 128K output context, $0.5/1M — newest flagship from the GLM family |
-| 🔮 **MiniMax M2.5** | Reasoning + agentic tasks at $0.30/1M — significant upgrade from M2.1 |
-| 🎯 **toolCalling Flag per Model** | Per-model `toolCalling: true/false` in registry — AutoCombo skips non-tool-capable models |
-| 🌍 **Multilingual Intent Detection** | PT/ZH/ES/AR keywords in AutoCombo scoring — better model selection for non-English content |
-| 📊 **Benchmark-Driven Fallbacks** | Real p95 latency from live requests feeds combo scoring — AutoCombo learns from actual data |
-| 🔁 **Request Deduplication** | Content-hash based dedup window — multi-agent safe, prevents duplicate charges |
-| 🔌 **Pluggable RouterStrategy** | Extensible `RouterStrategy` interface — add custom routing logic as plugins |
-
-### 🚀 Previous v2.0.9+ — Playground, CLI Fingerprints & ACP
-
-| Feature | What It Does |
+| ⚡ **Grok-4 Fast Family** | مدلهای xAI با قیمت 0.20 دلار / 0.50 دلار در میلیون - 1143 میلیثانیه (30 درصد سریعتر از فلش Gemini 2.5) محکگذاری شده |
+| 🧠 **GLM-5 از طریق Z.AI** | زمینه خروجی 128 هزار دلاری، 0.5/1 میلیون دلار — جدیدترین پرچمدار خانواده GLM |
+| 🔮 **MiniMax M2.5** | استدلال + وظایف نمایندگی با 0.30 دلار / 1 میلیون دلار - ارتقاء قابل توجه از M2.1 |
+| 🎯 **ابزار Calling Flag در هر مدل** | هر مدل `toolCalling: true/false` در رجیستری — AutoCombo مدلهای غیرقابل ابزار را حذف میکند |
+| 🌍 **تشخیص قصد چند زبانه** | کلمات کلیدی PT/ZH/ES/AR در امتیازدهی AutoCombo — انتخاب مدل بهتر برای محتوای غیر انگلیسی |
+| 📊 **معیارهای بازگشتی** | تأخیر واقعی p95 از درخواستهای زنده امتیازدهی ترکیبی — AutoCombo از دادههای واقعی یاد میگیرد |
+| 🔁 **درخواست حذف مجدد ** | پنجره dedup مبتنی بر هش محتوا — ایمن چند عاملی، از هزینه های تکراری جلوگیری می کند |
+| 🔌 **استراتژی روتر قابل اتصال** | رابط توسعه پذیر `RouterStrategy` — اضافه کردن منطق مسیریابی سفارشی به عنوان پلاگین |
+
+### 🚀 قبلی نسخه 2.0.9+ — زمین بازی، اثر انگشت CLI و ACP
+
+| ویژگی | چه می کند |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
-| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
-| 🤖 **ACP Agents Dashboard** | Debug › Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool. **OpenCode** users get a "Download opencode.json" button that auto-generates a ready-to-use config with all available models. |
-| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
-| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
-| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
+| 🎮 **مدل زمین بازی** | صفحه داشبورد برای آزمایش مستقیم هر مدل — ارائه دهنده/مدل/انتخاب کننده نقطه پایانی، ویرایشگر موناکو، پخش جریانی، لغو، زمان بندی |
+| 🔏 **تطابق اثر انگشت CLI** | سفارش سرصفحه/بدنه هر ارائه دهنده برای مطابقت با امضاهای بومی CLI — در تنظیمات > امنیت، هر ارائه دهنده را تغییر دهید. **IP پروکسی شما حفظ می شود** |
+| 🤖 **داشبورد نمایندگان ACP** | اشکال زدایی › صفحه نمایندگان - شبکه ای از 14 عامل با وضعیت نصب، نسخه، فرم عامل سفارشی برای هر ابزار CLI. **کاربران OpenCode** دکمه "دانلود opencode.json" را دریافت می کنند که به طور خودکار یک پیکربندی آماده برای استفاده را با همه مدل های موجود ایجاد می کند. |
+| 🔧 **مدل سفارشی `apiFormat` مسیریابی** | مدلهای سفارشی با `apiFormat: "responses"` اکنون به درستی به مترجم Responses API میروند |
+| 🏢 **Codex جداسازی فضای کاری** | چندین فضای کاری Codex در هر ایمیل — OAuth به درستی اتصالات را با شناسه فضای کاری جدا می کند |
+| 🔄 **به روز رسانی خودکار الکترونیک** | برنامه دسکتاپ برای به روز رسانی ها + نصب خودکار در راه اندازی مجدد |
-### 🤖 Agent & Protocol Operations (v2.0)
+### 🤖 عملیات عامل و پروتکل (نسخه 2.0)
-| Feature | What It Does |
+| ویژگی | چه می کند |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 🔧 **MCP Server (107 tools)** | IDE/agent tools via 3 transports: stdio, SSE (`/api/mcp/sse`), Streamable HTTP (`/api/mcp/stream`). 107 unique tools across the registered tool families; enabled skills may add dynamic tools at runtime |
-| 🤝 **A2A Server (JSON-RPC + SSE)** | Agent-to-agent task execution with sync and streaming flows |
-| 🧭 **Consolidated Endpoints Page** | Tabbed management page with Endpoint Proxy, MCP, A2A, and API Endpoints tabs |
-| 🎚️ **Service Enable/Disable Toggles** | ON/OFF switches for MCP and A2A with settings persistence (default: OFF) |
-| 🛰️ **MCP Runtime Heartbeat** | Real process status (pid, uptime, heartbeat age, transport, scope mode) |
-| 📋 **MCP Audit Trail** | Filterable audit logs with success/failure and key attribution |
-| 🔐 **MCP Scope Enforcement** | 32 granular scope permissions for controlled tool access |
-| 📡 **A2A Task Lifecycle Management** | List/filter tasks, inspect events/artifacts, cancel running tasks |
-| 📋 **Agent Card Discovery** | `/.well-known/agent.json` for client auto-discovery |
-| 🧪 **Protocol E2E Test Harness** | Real MCP SDK + A2A client flows in `test:protocols:e2e` |
-| ⚙️ **Operational Controls** | Switch combos, tune resilience settings, and review breaker state from dedicated Health and Settings surfaces |
-
-### 🧠 Routing & Intelligence
-
-| Feature | What It Does |
+| 🔧 **سرور MCP (107 ابزار)** | ابزار IDE/عامل از طریق 3 انتقال: stdio، SSE (`/api/mcp/sse`)، HTTP قابل جریان (`/api/mcp/stream`). 107 ابزار منحصر به فرد در میان خانواده ابزارهای ثبت شده؛ مهارتهای فعال ممکن است ابزارهای پویا را در زمان اجرا اضافه کنند |
+| 🤝 **سرور A2A (JSON-RPC + SSE)** | اجرای کار عامل به عامل با همگام سازی و جریان های جریانی |
+| 🧭 **صفحه نقاط پایانی تلفیقی** | صفحه مدیریت زبانهدار با برگههای Endpoint Proxy، MCP، A2A، و API Endpoints |
+| 🎚️ **سرویس فعال/غیرفعال کردن ضامن ها** | کلیدهای روشن/خاموش برای MCP و A2A با تداوم تنظیمات (پیشفرض: OFF) |
+| 🛰️ **MCP زمان اجرا Heartbeat** | وضعیت واقعی فرآیند (pid، uptime، سن ضربان قلب، حمل و نقل، حالت دامنه) |
+| 📋 **MCP مسیر حسابرسی** | گزارش های حسابرسی قابل فیلتر با موفقیت/شکست و اسناد کلیدی |
+| 🔐 ** MCP Scope Enforcement ** | 32 مجوز granular scope برای دسترسی به ابزار کنترل شده |
+| 📡 **A2A مدیریت چرخه حیات وظیفه** | فهرست/فیلتر کردن وظایف، بازرسی رویدادها/مصنوعات، لغو وظایف در حال اجرا |
+| 📋 **کشف کارت نماینده** | `/.well-known/agent.json` برای کشف خودکار مشتری |
+| 🧪 **آرنج تست پروتکل E2E** | Real MCP SDK + A2A کلاینت در `test:protocols:e2e` جریان می یابد |
+| ⚙️ **کنترل های عملیاتی** | جابجایی ترکیبها، تنظیم تنظیمات انعطافپذیری و بررسی وضعیت شکن از سطوح اختصاصی Health و Settings |
+
+### 🧠 مسیریابی و هوشمندی
+
+| ویژگی | چه می کند |
| ---------------------------------- | ------------------------------------------------------------------------ |
-| 🎯 **Smart 4-Tier Fallback** | Auto-route: Subscription → API Key → Cheap → Free |
-| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown per provider |
-| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Responses with schema-safe conversions |
-| 👥 **Multi-Account Support** | Multiple accounts per provider with intelligent selection |
-| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically with retry |
-| 🎨 **Custom Combos** | 13 balancing strategies + fallback chain control |
-| 🔗 **Context Relay** | Session continuity handoffs when account rotation happens mid-session |
-| 🌐 **Wildcard Router** | `provider/*` dynamic routing |
-| 🧠 **Thinking Budget Controls** | Passthrough, auto, custom, and adaptive reasoning limits |
-| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
-| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
-| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
-| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
-| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
-| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
-| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
-| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
-
-### 🎵 Multi-Modal APIs
-
-| Feature | What It Does |
+| 🎯 **هوشمند 4 طبقه بازگشتی ** | مسیر خودکار: اشتراک → کلید API → ارزان → رایگان |
+| 📊 **ردیابی سهمیه بی درنگ** | تعداد توکن زنده + بازنشانی شمارش معکوس برای هر ارائه دهنده |
+| 🔄 **ترجمه فرمت** | OpenAI ↔ Claude ↔ Gemini ↔ پاسخ با تبدیل های ایمن طرحواره |
+| 👥 **پشتیبانی چند حساب ** | چندین حساب در هر ارائه دهنده با انتخاب هوشمند |
+| 🔄 **بازسازی خودکار توکن** | توکن های OAuth به طور خودکار با تلاش مجدد |
+| 🎨 **ترکیب های سفارشی** | 13 استراتژی متعادل سازی + کنترل زنجیره ای بازگشتی |
+| 🔗 **رله زمینه** | وقتی چرخش حساب در اواسط جلسه اتفاق میافتد، تداوم جلسه ارسال میشود |
+| 🌐 **روتر Wildcard** | مسیریابی پویا `provider/*` |
+| 🧠 **تفکر کنترل های بودجه ** | محدودیت های استدلال گذرا، خودکار، سفارشی و تطبیقی |
+| 🔀 **نام مستعار مدل** | داخلی + مدل سفارشی نامگذاری و امنیت مهاجرت |
+| ⚡ **تخریب پس زمینه** | مسیریابی وظایف پس زمینه با اولویت پایین به مدل های ارزان تر |
+| 🧪 **مسیریابی هوشمند Task-Aware** | انتخاب خودکار مدل بر اساس نوع محتوا (کدگذاری/دید/تحلیل/خلاصه) |
+| 🔄 ** گردش کار نماینده A2A ** | ارکستراتور قطعی FSM برای اجرای چند مرحله ای عامل |
+| 🔀 **مسیریابی تطبیقی** | نادیده گرفتن استراتژی پویا بر اساس حجم توکن و پیچیدگی سریع |
+| 🎲 **تنوع ارائه دهنده** | توزیع ترافیک خودکار ترکیبی متعادل کننده امتیاز آنتروپی شانون |
+| 💬 **تزریق سریع سیستم** | کنترل های رفتار جهانی به طور مداوم اعمال می شود |
+| 📄 **پاسخ ها سازگاری API ** | پشتیبانی کامل از `/v1/responses` برای Codex و گردش کار نمایندگی پیشرفته |
+
+### 🎵 API های چند وجهی
+
+| ویژگی | چه می کند |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 🖼️ **Image Generation** | `/v1/images/generations` with cloud and local backends |
-| 📐 **Embeddings** | `/v1/embeddings` for search and RAG pipelines |
-| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
-| 🔊 **Text-to-Speech** | `/v1/audio/speech` — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) with correct error messages |
-| 🎬 **Video Generation** | `/v1/videos/generations` (ComfyUI + SD WebUI workflows) |
-| 🎵 **Music Generation** | `/v1/music/generations` (ComfyUI workflows) |
-| 🛡️ **Moderations** | `/v1/moderations` safety checks |
-| 🔀 **Reranking** | `/v1/rerank` for relevance scoring |
-| 🔍 **Web Search** 🆕 | `/v1/search` — 5 providers (Serper, Brave, Perplexity, Exa, Tavily), 6,500+ free/month, auto-failover, cache |
-
-### 🛡️ Resilience, Security & Governance
-
-| Feature | What It Does |
+| 🖼️ **تولید تصویر** | `/v1/images/generations` با ابر و باطن محلی |
+| 📐 **جاسازی ها** | `/v1/embeddings` برای جستجو و خطوط لوله RAG |
+| 🎤 **رونویسی صوتی** | `/v1/audio/transcriptions` — 7 ارائه دهنده (Deepgram Nova 3، AssemblyAI، Groq Whisper، HuggingFace، ElevenLabs، OpenAI، Azure)، تشخیص خودکار زبان، پشتیبانی MP4/MP3/WAV |
+| 🔊 **متن به گفتار** | `/v1/audio/speech` — 10 ارائه دهنده (ElevenLabs، OpenAI، Deepgram، Cartesia، PlayHT، HuggingFace، Nvidia NIM، Inworld، Coqui، Tortoise) با پیام های خطای صحیح |
+| 🎬 **نسل ویدیو** | `/v1/videos/generations` (گردش های کاری ComfyUI + SD WebUI) |
+| 🎵 **نسل موسیقی** | `/v1/music/generations` (جریان کاری ComfyUI) |
+| 🛡️ **اعتدال ** | `/v1/moderations` بررسی های ایمنی |
+| 🔀 **رتبه بندی مجدد ** | `/v1/rerank` برای امتیازدهی مرتبط |
+| 🔍 **جستجوی وب** 🆕 | `/v1/search` — 5 ارائه دهنده (Serper، Brave، Perplexity، Exa، Tavily)، بیش از 6500 رایگان در ماه، خودکار failover، کش |
+
+### 🛡️ تاب آوری، امنیت و حکمرانی
+
+| ویژگی | چه می کند |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------- |
-| 🔌 **Provider Circuit Breakers** | Provider-wide trip/recover after fallback exhaustion with configurable thresholds |
-| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight |
-| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
-| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events |
-| 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers |
-| ⚡ **Request Idempotency** | Duplicate protection window |
-| 🔒 **TLS Fingerprint Spoofing** | Browser-like TLS fingerprint — **reduces bot detection and account flagging** |
-| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
-| 🌐 **IP Filtering** | Allowlist/blocklist control for exposed deployments |
-| 🚦 **Request Queue & Pacing** | Configurable per-connection request buckets for RPM, spacing, concurrency, and max wait |
-| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
-| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
-| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
-| ❄️ **Connection Cooldown** | Retryable 408/429/5xx failures cool down a single connection with optional upstream hints |
-| 🚪 **Auto-Disable Banned Accounts** | Permanently blocked token accounts can be disabled automatically |
-| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
-| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
-| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
-| 🛡️ **Safe Outbound Fetch** 🆕 | Guarded fetch for provider calls — blocks private/local URLs, retries, SSRF protection |
-| ⏳ **Wait For Cooldown** 🆕 | Auto-retry chat after connection cooldowns; configurable `enabled`, `maxRetries`, and `maxRetryWaitSec` |
-| 🔍 **Runtime Env Validation** 🆕 | Zod-based env schema validation at startup with actionable error messages |
-| 📋 **Compliance Audit v2** 🆕 | Pagination, request context, auth events, provider CRUD, and SSRF-blocked logging |
-
-### 📊 Observability & Analytics
-
-| Feature | What It Does |
+| 🔌 **ارائه دهنده مدار شکن** | سفر/بازیابی در سراسر ارائه دهنده پس از خستگی مجدد با آستانه های قابل تنظیم |
+| 🔒 **قفل سهمیه روزانه** 🆕 | سیگنال های خستگی را تشخیص می دهد و مسیر را برای مدل خاص تا نیمه شب قفل می کند |
+| 🎯 **مدل های آگاه از نقطه پایانی** | مدل های سفارشی نقاط پایانی پشتیبانی شده + قالب API |
+| 🛡️ **گله ضد رعد** | محافظتهای Mutex + سمافور در رویدادهای امتحان مجدد/نرخ دادن |
+| 🧠 ** کش معنایی + امضا ** | کاهش هزینه/تأخیر با دو لایه کش |
+| ⚡ **درخواست عدم توانمندی** | پنجره حفاظتی تکراری |
+| 🔒 **تقلب اثر انگشت TLS** | اثر انگشت TLS شبیه مرورگر — **تشخیص ربات و پرچم گذاری حساب را کاهش می دهد** |
+| 🔏 **تطابق اثر انگشت CLI** | با امضاهای درخواستی CLI بومی مطابقت دارد — **با حفظ IP پروکسی خطر ممنوعیت را کاهش می دهد** |
+| 🌐 **فیلتر IP** | کنترل لیست مجاز/فهرست مسدود برای استقرارهای در معرض |
+| 🚦 **درخواست صف و سرعت** | قابل تنظیم سطل درخواست در هر اتصال برای RPM، فاصله، همزمانی، و حداکثر انتظار |
+| 📉 **تحقیر برازنده** | قابلیت چندلایه بازگشتی برای محافظت از عملیات دروازه اصلی |
+| 📜 **پیکربندی مسیر حسابرسی** | ردیابی تغییر مبتنی بر تفاوت که از رانش عملیاتی با برگشتهای ساده جلوگیری میکند |
+| ⏳ **همگام سازی ارائه دهنده سلامت** | پیشگیرانه نظارت بر انقضای توکن هشدارهای راه اندازی قبل از خرابی مجوز |
+| ❄️ **خنک کننده اتصال** | خطاهای 408/429/5xx قابل امتحان مجدد یک اتصال را با نکات بالادستی اختیاری خنک می کند |
+| 🚪 ** غیرفعال کردن خودکار حساب های ممنوعه ** | حسابهای توکن مسدود شده برای همیشه بهطور خودکار غیرفعال میشوند |
+| 🔑 **API کلید مدیریت + محدوده** | کنترل ایمن صدور/چرخش کلید و مدل/ارائه دهنده |
+| 👁️ **نمایش کلید API با محدوده** 🆕 | بازیابی کلیدهای API از طریق `ALLOW_API_KEY_REVEAL` |
+| 🛡️ **محافظت شده `/models`** | درگاه تأیید اختیاری و مخفی کردن ارائه دهنده برای کاتالوگ مدل |
+| 🛡️ ** واکشی ایمن خروجی** 🆕 | واکشی محافظت شده برای تماس های ارائه دهنده — URL های خصوصی/محلی را مسدود می کند، تلاش های مجدد، حفاظت SSRF |
+| ⏳ **منتظر خنک شدن باشید** 🆕 | سعی مجدد خودکار چت پس از خنک شدن اتصال. قابل تنظیم `enabled`، `maxRetries`، و `maxRetryWaitSec` |
+| 🔍 **Runtime Env Validation** 🆕 | اعتبار سنجی طرحواره env مبتنی بر Zod در هنگام راه اندازی با پیام های خطای قابل اجرا |
+| 📋 **ممیزی انطباق v2** 🆕 | صفحه بندی، زمینه درخواست، رویدادهای احراز هویت، ارائه دهنده CRUD، و ورود به سیستم مسدود شده با SSRF |
+
+### 📊 قابلیت مشاهده و تجزیه و تحلیل
+
+| ویژگی | چه می کند |
| -------------------------------- | ----------------------------------------------------- |
-| 📝 **Request + Proxy Logging** | Full request/response and proxy logging |
-| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI |
-| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers |
-| 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page |
-| 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing |
-| 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats |
-| 💰 **Cost Tracking** | Budget controls and per-model pricing visibility |
-| 📈 **Analytics Visualizations** | Model/provider usage insights and trend views |
-| 🧪 **Evaluation Framework** | Golden set testing with configurable match strategies |
-| 📡 **Live Diagnostics** 🆕 | Semantic cache bypass for accurate combo live testing |
-| 🔐 **TPS Log Metric** 🆕 | Tokens Per Second badge in log details modal |
-
-### ☁️ Deployment & Platform
-
-| Feature | What It Does |
+| 📝 **درخواست + ثبت پروکسی** | درخواست/پاسخ کامل و ثبت پروکسی |
+| 📉 **گزارشهای تفصیلی جریانی** | جریان های بار محموله SSE را به طور تمیز در UI بازسازی می کند |
+| 🏷️ **نشان های مدل بلادرنگ ** 🆕 | وضعیت مدل زنده و تایمرهای شمارش معکوس سهمیه روزانه |
+| 📋 **داشبورد گزارش های یکپارچه** | نماهای درخواست، پروکسی، ممیزی و کنسول در یک صفحه |
+| 🔍 **درخواست تله متری** | تأخیر p50/p95/p99 و ردیابی درخواست |
+| 🏥 **داشبورد سلامت** | Uptime، حالت های قطع کننده، قفل ها، آمار حافظه پنهان |
+| 💰 **پیگیری هزینه** | کنترل های بودجه و مشاهده قیمت گذاری هر مدل |
+| 📈 **تجسم های تحلیلی** | بینش استفاده از مدل/ارائه دهنده و نماهای روند |
+| 🧪 **چارچوب ارزشیابی** | تست مجموعه طلایی با استراتژی های تطبیق قابل تنظیم |
+| 📡 **تشخیص زنده** 🆕 | دور زدن حافظه پنهان معنایی برای آزمایش زنده ترکیبی دقیق |
+| 🔐 **TPS Log Metric** 🆕 | نشان توکن در ثانیه در جزئیات گزارش معین |
+
+### ☁️ استقرار و پلتفرم
+
+| ویژگی | چه می کند |
| ------------------------------ | --------------------------------------------------------------------- |
-| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloud environments |
-| 🚇 **Cloudflare Tunnel** 🆕 | One-click Quick Tunnel integration from the dashboard |
-| 🔑 **API Key Model Filtering** | Native /v1/models response filtered via assigned Bearer context roles |
-| ⚡ **Smart Cache Bypass** | Configurable TTL heuristics and forced refetch controls |
-| 🔄 **Backup/Restore** | Export/import and disaster recovery flows |
-| 🧙 **Onboarding Wizard** | First-run guided setup |
-| 🔧 **CLI Tools Dashboard** | One-click setup for popular coding tools |
-| 🎮 **Model Playground** | Test any provider/model/endpoint from the dashboard |
-| 🔏 **CLI Fingerprint Toggle** | Per-provider fingerprint matching in Settings > Security |
-| 🌐 **i18n (30 languages)** | Full dashboard + docs language support with RTL coverage |
-| 🧹 **Clear All Models** | One-click model list clearing in provider details |
-| 👁️ **Sidebar Controls** 🆕 | Hide components and integrations from Appearance Settings |
-| 📋 **Issue Templates** | Standardized GitHub templates for bugs and features |
-| 📂 **Custom Data Directory** | `DATA_DIR` override for storage location |
-| 🌐 **V1 WebSocket Bridge** 🆕 | OpenAI-compatible WebSocket traffic proxied via `/v1/ws` |
-| 🔑 **Sync Tokens & Bundle** 🆕 | Config sync tokens + versioned bundle endpoint with ETag support |
-
-### Feature Deep Dive
-
-#### Smart fallback with practical cost control
+| 🌐 **استقرار در هر کجا ** | Localhost، VPS، Docker، محیط های ابری |
+| 🚇 **Cloudflare تونل** 🆕 | ادغام Quick Tunnel با یک کلیک از داشبورد |
+| 🔑 ** فیلتر مدل کلید API ** | پاسخ بومی /v1/models از طریق نقشهای زمینه حامل اختصاص داده شده فیلتر شده است
+| ⚡ **عملیات کش هوشمند** | اکتشافی TTL قابل تنظیم و کنترل های بازیابی اجباری |
+| 🔄 **پشتیبان گیری/بازیابی** | صادرات/واردات و جریان های بازیابی بلایا |
+| 🧙 **جادوگر سوار شدن** | راه اندازی هدایت شده در اولین اجرا |
+| 🔧 **CLI Tools Dashboard ** | راه اندازی با یک کلیک برای ابزارهای کدنویسی محبوب |
+| 🎮 **مدل زمین بازی** | هر ارائه دهنده/مدل/نقطه پایانی را از داشبورد تست کنید |
+| 🔏 **ضامن اثر انگشت CLI** | مطابقت اثر انگشت هر ارائه دهنده در تنظیمات > امنیت |
+| 🌐 **i18n (30 زبان)** | داشبورد کامل + پشتیبانی از زبان اسناد با پوشش RTL |
+| 🧹 **پاک کردن همه مدل ها** | پاک کردن لیست مدل با یک کلیک در جزئیات ارائه دهنده |
+| 👁️ **کنترل های نوار کناری** 🆕 | مخفی کردن اجزا و ادغام ها از تنظیمات ظاهر |
+| 📋 **نمونه های شماره** | الگوهای استاندارد GitHub برای اشکالات و ویژگی ها |
+| 📂 **دایرکتوری داده های سفارشی** | لغو `DATA_DIR` برای مکان ذخیره سازی |
+| 🌐 **V1 WebSocket Bridge** 🆕 | ترافیک OpenAI سازگار با WebSocket پروکسی شده از طریق `/v1/ws` |
+| 🔑 **همگام سازی توکن ها و بسته ها** 🆕 | پیکربندی نشانههای همگامسازی + نسخه نهایی بسته نرمافزاری با پشتیبانی ETag |
+
+### ویژگی Deep Dive
+
+#### بازگشت هوشمند با کنترل هزینه عملی
```txt
Combo: "my-coding-stack"
@@ -1504,73 +1504,73 @@ Combo: "my-coding-stack"
4. if/kimi-k2-thinking
```
-When quota, rate, or health fails, OmniRoute automatically moves to the next candidate without manual switching.
+هنگامی که سهمیه، نرخ، یا سلامت ناموفق باشد، OmniRoute به طور خودکار بدون تغییر دستی به نامزد بعدی منتقل می شود.
-#### Protocol management that is visible and operable
+#### مدیریت پروتکل قابل مشاهده و قابل اجرا
-- MCP + A2A are discoverable in UI and docs (not hidden)
-- Protocol status APIs expose live operational data (`/api/mcp/*`, `/api/a2a/*`)
-- Dashboards include actions for day-2 ops (combo toggles, breaker resets, task cancellation)
+- MCP + A2A در رابط کاربری و اسناد قابل کشف هستند (پنهان نمی شوند)
+- APIهای وضعیت پروتکل دادههای عملیاتی زنده را در معرض دید قرار میدهند (`/api/mcp/*`، `/api/a2a/*`)
+- داشبوردها شامل اقداماتی برای عملیات روز دوم هستند (تغییرهای ترکیبی، بازنشانی قطع کننده، لغو کار)
-#### Translator + validation workflow
+#### مترجم + گردش کار اعتبار سنجی
-The Translator area includes:
+منطقه مترجم شامل:
-- **Playground**: request transformation checks
-- **Chat Tester**: full request/response round-trip
-- **Test Bench**: multiple cases in one run
-- **Live Monitor**: real-time traffic view
+- **زمین بازی**: درخواست بررسی تبدیل
+- **تستر چت**: درخواست/پاسخ کامل رفت و برگشت
+- **میز تست **: موارد متعدد در یک اجرا
+- ** مانیتور زنده **: نمایش ترافیک در زمان واقعی
-Plus protocol validation with real clients via `npm run test:protocols:e2e`.
+اعتبار سنجی پروتکل پلاس با مشتریان واقعی از طریق `npm run test:protocols:e2e`.
-> 📖 **[MCP Server README](open-sse/mcp-server/README.md)** — Tool reference, IDE configs, and client examples
+> 📖 **[MCP سرور README](open-sse/mcp-server/README.md)** — مرجع ابزار، تنظیمات IDE و نمونه های مشتری
>
-> 📖 **[A2A Server README](src/lib/a2a/README.md)** — Skills, JSON-RPC methods, streaming, and task lifecycle
+> 📖 **[A2A سرور README](src/lib/a2a/README.md)** — مهارت ها، روش های JSON-RPC، جریان و چرخه عمر کار
-## 🧪 Evaluations (Evals)
+## 🧪 ارزیابی ها (ارزیابی ها)
-OmniRoute includes a built-in evaluation framework to test LLM response quality against a golden set. Access it via **Analytics → Evals** in the dashboard.
+OmniRoute شامل یک چارچوب ارزیابی داخلی برای آزمایش کیفیت پاسخ LLM در برابر یک مجموعه طلایی است. از طریق **Analytics → Evals** در داشبورد به آن دسترسی داشته باشید.
-### Built-in Golden Set
+### ست طلایی توکار
-The pre-loaded "OmniRoute Golden Set" contains test cases for:
+«مجموعه طلایی OmniRoute» از پیش بارگذاری شده حاوی موارد آزمایشی برای موارد زیر است:
-- Greetings, math, geography, code generation
-- JSON format compliance, translation, markdown generation
-- Safety refusal (harmful content), counting, boolean logic
+- با سلام، ریاضی، جغرافیا، تولید کد
+- مطابقت با فرمت JSON، ترجمه، تولید علامت گذاری
+- امتناع ایمنی (محتوای مضر)، شمارش، منطق بولی
-### Evaluation Strategies
+### استراتژی های ارزیابی
-| Strategy | Description | Example |
+| استراتژی | توضیحات | مثال |
| ---------- | ------------------------------------------------ | -------------------------------- |
-| `exact` | Output must match exactly | `"4"` |
-| `contains` | Output must contain substring (case-insensitive) | `"Paris"` |
-| `regex` | Output must match regex pattern | `"1.*2.*3"` |
-| `custom` | Custom JS function returns true/false | `(output) => output.length > 10` |
+| `exact` | خروجی باید دقیقاً مطابقت داشته باشد | `"4"` |
+| `contains` | خروجی باید حاوی رشته فرعی (بدون حساس به بزرگی) | `"Paris"` |
+| `regex` | خروجی باید با الگوی regex مطابقت داشته باشد | `"1.*2.*3"` |
+| `custom` | تابع JS سفارشی true/false | `(output) => output.length > 10` |
---
-## 📖 Setup Guide
+## 📖 راهنمای راه اندازی
-### Protocol Setup (MCP + A2A)
+### راه اندازی پروتکل (MCP + A2A)
🧩 MCP Setup (Model Context Protocol)
-Start MCP transport in stdio mode:
+حمل و نقل MCP را در حالت stdio شروع کنید:
```bash
omniroute --mcp
```
-Recommended validation flow:
+جریان اعتبارسنجی توصیه شده:
-1. Connect your MCP client over stdio.
-2. Run `omniroute_get_health`.
-3. Run `omniroute_list_combos`.
-4. Open `/dashboard/mcp` to confirm heartbeat, activity, and audit.
+1. مشتری MCP خود را از طریق stdio وصل کنید.
+2. `omniroute_get_health` را اجرا کنید.
+3. `omniroute_list_combos` را اجرا کنید.
+4. `/dashboard/mcp` را برای تأیید ضربان قلب، فعالیت و ممیزی باز کنید.
-Useful APIs for automation:
+APIهای مفید برای اتوماسیون:
- `GET /api/mcp/status`
- `GET /api/mcp/tools`
@@ -1582,13 +1582,13 @@ Useful APIs for automation:
🤝 A2A Setup (Agent2Agent)
-Discover the agent:
+عامل را کشف کنید:
```bash
curl http://localhost:20128/.well-known/agent.json
```
-Send a task:
+ارسال یک کار:
```bash
curl -X POST http://localhost:20128/a2a \
@@ -1596,40 +1596,40 @@ curl -X POST http://localhost:20128/a2a \
-d '{"jsonrpc":"2.0","id":"setup-a2a","method":"message/send","params":{"skill":"quota-management","messages":[{"role":"user","content":"Summarize quota status."}]}}'
```
-Manage lifecycle:
+مدیریت چرخه عمر:
- `GET /api/a2a/status`
- `GET /api/a2a/tasks`
- `GET /api/a2a/tasks/:id`
- `POST /api/a2a/tasks/:id/cancel`
-Operational UI:
+رابط کاربری عملیاتی:
-- `/dashboard/a2a` for task/state/stream observability and smoke actions
+- `/dashboard/a2a` برای قابلیت مشاهده وظیفه/وضعیت/جریان و اقدامات دود
🧪 End-to-end protocol validation
-Validate both protocols with real clients:
+هر دو پروتکل را با کلاینت های واقعی اعتبار سنجی کنید:
```bash
npm run test:protocols:e2e
```
-This verifies:
+این تأیید می کند:
-- MCP SDK client connect/list/call
-- A2A discovery/send/stream/get/cancel
-- Cross-check data in MCP audit and A2A task management APIs
+- اتصال/لیست/تماس کلاینت MCP SDK
+- A2A کشف / ارسال / جریان / دریافت / لغو
+- بررسی متقاطع داده ها در ممیزی MCP و APIهای مدیریت وظایف A2A
💳 Subscription Providers
-### Claude Code (Pro/Max)
+کد ### Claude (Pro/Max)
```bash
Dashboard → Providers → Connect Claude Code
@@ -1642,7 +1642,7 @@ Models:
cc/claude-haiku-4-5-20251001
```
-**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. OmniRoute tracks quota per model!
+**نکته حرفه ای:** از Opus برای کارهای پیچیده و Sonnet برای سرعت استفاده کنید. OmniRoute سهمیه هر مدل را دنبال می کند!
### OpenAI Codex (Plus/Pro)
@@ -1656,22 +1656,22 @@ Models:
cx/gpt-5.1-codex-max
```
-#### Codex Account Limit Management (5h + Weekly)
+#### Codex مدیریت محدودیت حساب (5 ساعت + هفتگی)
-Each Codex account now has policy toggles in `Dashboard -> Providers`:
+هر حساب Codex اکنون دارای تغییر سیاست در `Dashboard -> Providers` است:
-- `5h` (ON/OFF): enforce the 5-hour window threshold policy.
-- `Weekly` (ON/OFF): enforce the weekly window threshold policy.
-- Threshold behavior: when an enabled window reaches >=90% usage, that account is skipped.
-- Rotation behavior: OmniRoute routes to the next eligible Codex account automatically.
-- Reset behavior: when the provider `resetAt` time passes, the account becomes eligible again automatically.
+- `5h` (روشن/خاموش): سیاست آستانه پنجره 5 ساعته را اجرا کنید.
+- `Weekly` (روشن/خاموش): سیاست آستانه پنجره هفتگی را اجرا کنید.
+- رفتار آستانه: هنگامی که یک پنجره فعال به >=90 درصد استفاده می رسد، آن حساب حذف می شود.
+- رفتار چرخش: OmniRoute به طور خودکار به حساب بعدی واجد شرایط Codex مسیر میدهد.
+- رفتار بازنشانی: هنگامی که زمان ارائه دهنده `resetAt` می گذرد، حساب دوباره به طور خودکار واجد شرایط می شود.
Scenarios:
-- `5h ON` + `Weekly ON`: account is skipped when either window reaches threshold.
-- `5h OFF` + `Weekly ON`: only weekly usage can block the account.
-- `5h ON` + `Weekly OFF`: only 5-hour usage can block the account.
-- `resetAt` passed: account re-enters rotation automatically (no manual re-enable).
+- `5h ON` + `Weekly ON`: هنگامی که هر یک از پنجره ها به آستانه می رسد حساب حذف می شود.
+- `5h OFF` + `Weekly ON`: فقط استفاده هفتگی می تواند حساب را مسدود کند.
+- `5h ON` + `Weekly OFF`: فقط استفاده 5 ساعته می تواند حساب را مسدود کند.
+- `resetAt` تصویب شد: حساب به طور خودکار دوباره وارد چرخش می شود (بدون فعال کردن مجدد دستی).
### GitHub Copilot
@@ -1691,88 +1691,88 @@ Models:
🔑 API Key Providers
-### NVIDIA NIM (FREE developer access — 70+ models)
+### NVIDIA NIM (دسترسی رایگان برنامه نویس - بیش از 70 مدل)
-1. Sign up: [build.nvidia.com](https://build.nvidia.com)
-2. Get free API key (1000 inference credits included)
-3. Dashboard → Add Provider → NVIDIA NIM:
- - API Key: `nvapi-your-key`
+1. ثبت نام کنید: [build.nvidia.com](https://build.nvidia.com)
+2. کلید API رایگان دریافت کنید (1000 اعتبار استنتاج شامل)
+3. داشبورد → افزودن ارائه دهنده → NVIDIA NIM:
+ - کلید API: `nvapi-your-key`
-**Models:** `nvidia/llama-3.3-70b-instruct`, `nvidia/mistral-7b-instruct`, and 50+ more
+**مدل ها:** `nvidia/llama-3.3-70b-instruct`، `nvidia/mistral-7b-instruct`، و بیش از 50 مدل دیگر
-**Pro Tip:** OpenAI-compatible API — works seamlessly with OmniRoute's format translation!
+** نکته حرفه ای: ** OpenAI سازگار با API - با ترجمه فرمت OmniRoute یکپارچه کار می کند!
### DeepSeek
-1. Sign up: [platform.deepseek.com](https://platform.deepseek.com)
-2. Get API key
-3. Dashboard → Add Provider → DeepSeek
+1. ثبت نام کنید: [platform.deepseek.com](https://platform.deepseek.com)
+2. کلید API را دریافت کنید
+3. داشبورد → افزودن ارائه دهنده → DeepSeek
-**Models:** `deepseek/deepseek-chat`, `deepseek/deepseek-coder`
+**مدل ها:** `deepseek/deepseek-chat`، `deepseek/deepseek-coder`
-### Groq (Free Tier Available!)
+### Groq (سطح رایگان موجود است!)
-1. Sign up: [console.groq.com](https://console.groq.com)
-2. Get API key (free tier included)
-3. Dashboard → Add Provider → Groq
+1. ثبت نام کنید: [console.groq.com](https://console.groq.com)
+2. کلید API را دریافت کنید (شامل ردیف رایگان)
+3. داشبورد → افزودن ارائه دهنده → Groq
-**Models:** `groq/llama-3.3-70b`, `groq/mixtral-8x7b`
+**مدل ها:** `groq/llama-3.3-70b`، `groq/mixtral-8x7b`
-**Pro Tip:** Ultra-fast inference — best for real-time coding!
+** نکته حرفه ای: ** استنتاج فوق العاده سریع - بهترین برای برنامه نویسی در زمان واقعی!
-### OpenRouter (100+ Models)
+### OpenRouter (100+ مدل)
-1. Sign up: [openrouter.ai](https://openrouter.ai)
-2. Get API key
-3. Dashboard → Add Provider → OpenRouter
+1. ثبت نام کنید: [openrouter.ai](https://openrouter.ai)
+2. کلید API را دریافت کنید
+3. داشبورد → افزودن ارائه دهنده → OpenRouter
-**Models:** Access 100+ models from all major providers through a single API key.
+**مدل ها:** از طریق یک کلید API به بیش از 100 مدل از همه ارائه دهندگان اصلی دسترسی پیدا کنید.
-**Dashboard behavior:** OpenRouter models are managed from **Available Models**. Manual add, import, and auto-sync all update the same list.
+**رفتار داشبورد:** مدل های OpenRouter از **مدل های موجود** مدیریت می شوند. افزودن دستی، وارد کردن، و همگامسازی خودکار همه یک لیست را بهروزرسانی میکنند.
💰 Cheap Providers (Backup)
-### GLM-4.7 (Daily reset, $0.6/1M)
+### GLM-4.7 (بازنشانی روزانه، 0.6/1 میلیون دلار)
-1. Sign up: [Zhipu AI](https://open.bigmodel.cn/)
-2. Get API key from Coding Plan
-3. Dashboard → Add API Key:
- - Provider: `glm`
- - API Key: `your-key`
+1. ثبت نام: [Zhipu AI](https://open.bigmodel.cn/)
+2. کلید API را از برنامه کدگذاری دریافت کنید
+3. داشبورد ← افزودن کلید API:
+ - ارائه دهنده: `glm`
+ - کلید API: `your-key`
-**Use:** `glm/glm-4.7`
+**استفاده:** `glm/glm-4.7`
-**Pro Tip:** Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM.
+** نکته حرفه ای: ** طرح برنامه نویسی سهمیه 3× را با هزینه 1/7 ارائه می دهد! بازنشانی روزانه 10:00 صبح.
-### MiniMax M2.1 (5h reset, $0.20/1M)
+### MiniMax M2.1 (5 ساعت بازنشانی، 0.20 دلار/1 میلیون دلار)
-1. Sign up: [MiniMax](https://www.minimax.io/)
-2. Get API key
-3. Dashboard → Add API Key
+1. ثبت نام کنید: [MiniMax](https://www.minimax.io/)
+2. کلید API را دریافت کنید
+3. داشبورد ← افزودن کلید API
-**Use:** `minimax/MiniMax-M2.1`
+**استفاده:** `minimax/MiniMax-M2.1`
-**Pro Tip:** Cheapest option for long context (1M tokens)!
+** نکته حرفه ای: ** ارزان ترین گزینه برای زمینه طولانی (1 میلیون توکن)!
-### Kimi K2 ($9/month flat)
+### Kimi K2 (9 دلار در ماه ثابت)
-1. Subscribe: [Moonshot AI](https://platform.moonshot.ai/)
-2. Get API key
-3. Dashboard → Add API Key
+1. مشترک شوید: [Moonshot AI](https://platform.moonshot.ai/)
+2. کلید API را دریافت کنید
+3. داشبورد ← افزودن کلید API
-**Use:** `kimi/kimi-latest`
+**استفاده:** `kimi/kimi-latest`
-**Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost!
+**نکته حرفه ای:** ثابت 9 دلار در ماه برای 10 میلیون توکن = 0.90 دلار / 1 میلیون هزینه موثر!
🆓 FREE Providers (Emergency Backup)
-### Qoder (5 FREE models via OAuth)
+### Qoder (5 مدل رایگان از طریق OAuth)
```bash
Dashboard → Connect Qoder
@@ -1787,7 +1787,7 @@ Models:
if/deepseek-r1
```
-### Qwen (4 FREE models via Device Code)
+### Qwen (4 مدل رایگان از طریق کد دستگاه)
```bash
Dashboard → Connect Qwen
@@ -1799,7 +1799,7 @@ Models:
qw/qwen3-coder-flash
```
-### Kiro (Claude FREE)
+### Kiro (رایگان Claude)
```bash
Dashboard → Connect Kiro
@@ -1816,7 +1816,7 @@ Models:
🎨 Create Combos
-### Example 1: Maximize Subscription → Cheap Backup
+### مثال 1: حداکثر کردن اشتراک → پشتیبان گیری ارزان
```
Dashboard → Combos → Create New
@@ -1830,7 +1830,7 @@ Models:
Use in CLI: premium-coding
```
-### Example 2: Free-Only (Zero Cost)
+### مثال 2: فقط رایگان (هزینه صفر)
```
Name: free-combo
@@ -1855,9 +1855,9 @@ Settings → Models → Advanced:
Model: cc/claude-opus-4-7
```
-### Claude Code
+### کد Claude
-Use the **CLI Tools** page in the dashboard for one-click configuration, or edit `~/.claude/settings.json` manually.
+از صفحه **CLI Tools** در داشبورد برای پیکربندی با یک کلیک استفاده کنید، یا `~/.claude/settings.json` را به صورت دستی ویرایش کنید.
### Codex CLI
@@ -1870,13 +1870,13 @@ codex "your prompt"
### OpenClaw
-**Option 1 — Dashboard (recommended):**
+**گزینه 1 - داشبورد (توصیه می شود):**
```
Dashboard → CLI Tools → OpenClaw → Select Model → Apply
```
-**Option 2 — Manual:** Edit `~/.openclaw/openclaw.json`:
+**گزینه 2 — دستی:** ویرایش `~/.openclaw/openclaw.json`:
```json
{
@@ -1892,7 +1892,7 @@ Dashboard → CLI Tools → OpenClaw → Select Model → Apply
}
```
-> **Note:** OpenClaw only works with local OmniRoute. Use `127.0.0.1` instead of `localhost` to avoid IPv6 resolution issues.
+> **توجه:** OpenClaw فقط با OmniRoute محلی کار می کند. از `127.0.0.1` به جای `localhost` برای جلوگیری از مشکلات وضوح IPv6 استفاده کنید.
### Cline / Continue / RooCode
@@ -1906,7 +1906,7 @@ Settings → API Configuration:
### OpenCode
-**Step 1:** Add OmniRoute as a custom provider:
+**مرحله 1:** OmniRoute را به عنوان یک ارائه دهنده سفارشی اضافه کنید:
```bash
opencode
@@ -1914,7 +1914,7 @@ opencode
# Select "Other" → Enter ID: "omniroute" → Enter your OmniRoute API key
```
-**Step 2:** Create/edit `opencode.json` in your project root:
+**مرحله 2:** `opencode.json` را در ریشه پروژه خود ایجاد/ویرایش کنید:
```json
{
@@ -1936,123 +1936,123 @@ opencode
}
```
-**Step 3:** Select the model in OpenCode:
+**مرحله 3:** مدل را در OpenCode انتخاب کنید:
```bash
/models
# Select any OmniRoute model from the list
```
-> **Tip:** Add any model available in your OmniRoute `/v1/models` endpoint to the `models` section. Use the format `provider/model-id` from your OmniRoute dashboard.
+> **نکته:** هر مدل موجود در نقطه پایانی OmniRoute `/v1/models` خود را به بخش `models` اضافه کنید. از قالب `provider/model-id` از داشبورد OmniRoute خود استفاده کنید.
---
-## Solución de Problemas
+## راه حل مشکل
Click to expand troubleshooting guide
-**"Language model did not provide messages"**
+**"مدل زبان پیامی ارائه نکرد"**
-- Provider quota exhausted → Check dashboard quota tracker
-- Solution: Use combo fallback or switch to cheaper tier
+- سهمیه ارائه دهنده تمام شده است → ردیاب سهمیه داشبورد را بررسی کنید
+- راه حل: از ترکیبی جایگزین استفاده کنید یا به ردیف ارزان تر بروید
-**Rate limiting**
+**محدودیت نرخ**
-- Subscription quota out → Fallback to GLM/MiniMax
-- Add combo: `cc/claude-opus-4-7 → glm/glm-4.7 → if/kimi-k2-thinking`
+- سهمیه اشتراک → بازگشت به GLM/MiniMax
+- اضافه کردن ترکیبی: `cc/claude-opus-4-7 → glm/glm-4.7 → if/kimi-k2-thinking`
-**OAuth token expired**
+**توکن OAuth منقضی شده**
-- Auto-refreshed by OmniRoute
-- If issues persist: Dashboard → Provider → Reconnect
+- بهروزرسانی خودکار توسط OmniRoute
+- در صورت وجود مشکلات: داشبورد → ارائه دهنده → اتصال مجدد
-**High costs**
+**هزینه های بالا**
-- Check usage stats in Dashboard → Costs
-- Switch primary model to GLM/MiniMax
+- آمار استفاده را در داشبورد → هزینه ها بررسی کنید
+- تغییر مدل اولیه به GLM/MiniMax
-**Dashboard/API ports are wrong**
+**پورت های داشبورد/API اشتباه هستند**
-- `PORT` is the canonical base port (and API port by default)
-- `API_PORT` overrides only OpenAI-compatible API listener
-- `DASHBOARD_PORT` overrides only dashboard/Next.js listener
-- Set `NEXT_PUBLIC_BASE_URL` to your dashboard/public URL (for OAuth callbacks)
+- `PORT` پورت پایه متعارف است (و به طور پیش فرض درگاه API)
+- `API_PORT` فقط شنونده OpenAI سازگار با API را لغو می کند
+- `DASHBOARD_PORT` فقط شنونده داشبورد/Next.js را لغو می کند
+- `NEXT_PUBLIC_BASE_URL` را روی داشبورد/URL عمومی خود تنظیم کنید (برای تماس های OAuth)
-**Cloud sync errors**
+**خطاهای همگام سازی ابری**
-- Verify `BASE_URL` points to your running instance
-- Verify `CLOUD_URL` points to your expected cloud endpoint
-- Keep `NEXT_PUBLIC_*` values aligned with server-side values
+- نقاط `BASE_URL` را به نمونه در حال اجرا خود تأیید کنید
+- نقاط `CLOUD_URL` را به نقطه پایانی ابری مورد انتظار خود تأیید کنید
+- مقادیر `NEXT_PUBLIC_*` را با مقادیر سمت سرور هماهنگ نگه دارید
-**First login not working**
+**لاگین اول کار نمی کند**
-- Check `INITIAL_PASSWORD` in `.env`
-- If unset, fallback password is `123456`
+- `INITIAL_PASSWORD` را در `.env` بررسی کنید
+- اگر تنظیم نشده باشد، رمز عبور بازگشتی `123456` است
-**No request logs**
+**بدون گزارش درخواست**
-- `call_logs` in SQLite stores summary metadata for the Request Logs table and analytics views
-- Detailed request/response payloads are written to `DATA_DIR/call_logs/` as one JSON artifact per request
-- Enable pipeline capture from Dashboard → Logs → Request Logs if you need detailed per-stage payloads
-- `Export Logs` reads the artifact files on demand, while `Export All` includes the `call_logs/` directory alongside `storage.sqlite`
-- Set `APP_LOG_TO_FILE=true` if you also want application console logs in `logs/application/app.log`
-- Adjust `APP_LOG_MAX_FILE_SIZE`, `APP_LOG_RETENTION_DAYS`, `APP_LOG_MAX_FILES`, and `CALL_LOG_MAX_ENTRIES` as needed
+- `call_logs` در SQLite ابرداده های خلاصه را برای جدول Request Logs و نمایش های تجزیه و تحلیل ذخیره می کند
+- محموله های درخواست/پاسخ تفصیلی به `DATA_DIR/call_logs/` به عنوان یک مصنوع JSON در هر درخواست نوشته می شود
+- در صورت نیاز به محموله های دقیق در هر مرحله، ضبط خط لوله را از داشبورد → گزارش ها → گزارش های درخواستی فعال کنید
+- `Export Logs` فایل های مصنوع را در صورت درخواست می خواند، در حالی که `Export All` شامل فهرست `call_logs/` در کنار `storage.sqlite` است
+- اگر می خواهید گزارش های کنسول برنامه در `logs/application/app.log` نیز وجود داشته باشد، `APP_LOG_TO_FILE=true` را تنظیم کنید
+- `APP_LOG_MAX_FILE_SIZE`، `APP_LOG_RETENTION_DAYS`، `APP_LOG_MAX_FILES`، و `CALL_LOG_MAX_ENTRIES` را در صورت نیاز تنظیم کنید
-**Connection test shows "Invalid" for OpenAI-compatible providers**
+**آزمایش اتصال "نامعتبر" را برای ارائه دهندگان سازگار با OpenAI نشان می دهد**
-- Many providers don't expose a `/models` endpoint
-- OmniRoute v1.0.6+ includes fallback validation via chat completions
-- Ensure base URL includes `/v1` suffix
+- بسیاری از ارائه دهندگان نقطه پایانی `/models` را افشا نمی کنند
+- OmniRoute نسخه 1.0.6+ شامل اعتبار سنجی مجدد از طریق تکمیل چت است
+- مطمئن شوید که پایه URL دارای پسوند `/v1` است
-### 🔐 OAuth on a Remote Server
+### 🔐 OAuth در یک سرور راه دور
-> **⚠️ Important for users running OmniRoute on a VPS, Docker, or any remote server**
+> **⚠️ برای کاربرانی که OmniRoute را روی VPS، Docker یا هر سرور راه دور اجرا می کنند مهم است**
-The OAuth credentials bundled in OmniRoute are registered **for `localhost` only**. When you access OmniRoute on a remote server (e.g. `https://omniroute.myserver.com`), Google rejects the authentication with:
+اعتبارنامه OAuth همراه با OmniRoute **فقط برای `localhost`** ثبت شده است. هنگامی که به OmniRoute در یک سرور راه دور (مثلاً `https://omniroute.myserver.com`) دسترسی دارید، Google احراز هویت را با:
```
Error 400: redirect_uri_mismatch
```
-#### Solution: Configure your own OAuth credentials
+#### راه حل: اعتبار OAuth خود را پیکربندی کنید
-You need to create an **OAuth 2.0 Client ID** in Google Cloud Console with your server's URI.
+شما باید یک **OAuth 2.0 Client ID** در Google Cloud Console با URI سرور خود ایجاد کنید.
-#### Step-by-step
+#### گام به گام
-**1. Open Google Cloud Console**
+**1. باز کردن Google Cloud Console**
-Go to: [https://console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials)
+برو به: [https://console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials)
-**2. Create a new OAuth 2.0 Client ID**
+**2. یک شناسه مشتری OAuth 2.0 جدید ایجاد کنید**
-- Click **"+ Create Credentials"** → **"OAuth client ID"**
-- Application type: **"Web application"**
-- Name: anything you like (e.g. `OmniRoute Remote`)
+- روی **"+ ایجاد اعتبارنامه"** → **"OAuth شناسه مشتری"** کلیک کنید
+- نوع برنامه: **"برنامه وب"**
+- نام: هر چیزی که دوست دارید (به عنوان مثال `OmniRoute Remote`)
-**3. Add Authorized Redirect URIs**
+**3. URIهای مجاز تغییر مسیر را اضافه کنید**
-In the **"Authorized redirect URIs"** field, add:
+در قسمت **"URI های تغییر مسیر مجاز"**، اضافه کنید:
```
https://your-server.com/callback
```
-> Replace `your-server.com` with your server's domain or IP (include the port if needed, e.g. `http://45.33.32.156:20128/callback`).
+> `your-server.com` را با دامنه یا IP سرور خود جایگزین کنید (در صورت نیاز پورت را اضافه کنید، به عنوان مثال `http://45.33.32.156:20128/callback`).
-**4. Save and copy the credentials**
+**4. اطلاعات کاربری را ذخیره و کپی کنید**
-After creating, Google will show the **Client ID** and **Client Secret**.
+پس از ایجاد، Google **شناسه مشتری** و **Client Secret** را نشان می دهد.
-**5. Set environment variables**
+**5. تنظیم متغیرهای محیط **
-In your `.env` (or Docker environment variables):
+در `.env` (یا متغیرهای محیطی Docker):
```bash
# For Antigravity:
@@ -2063,7 +2063,7 @@ GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
```
-**6. Restart OmniRoute**
+**6. راه اندازی مجدد OmniRoute**
```bash
# npm:
@@ -2073,114 +2073,114 @@ npm run dev
docker restart omniroute
```
-**7. Try connecting again**
+**7. دوباره سعی کنید وصل شوید**
-Google will now redirect correctly to `https://your-server.com/callback`.
+Google اکنون به درستی به `https://your-server.com/callback` تغییر مسیر می دهد.
---
-#### Temporary workaround (without custom credentials)
+#### راه حل موقت (بدون اعتبارنامه های سفارشی)
-If you don't want to set up your own credentials right now, you can still use the **manual URL flow**:
+اگر نمیخواهید اعتبار خود را در حال حاضر تنظیم کنید، همچنان میتوانید از **جریان دستی URL** استفاده کنید:
-1. OmniRoute opens the Google authorization URL
-2. After authorizing, Google tries to redirect to `localhost` (which fails on the remote server)
-3. **Copy the full URL** from your browser's address bar (even if the page doesn't load)
-4. Paste that URL into the field shown in the OmniRoute connection modal
-5. Click **"Connect"**
+1. OmniRoute مجوز Google را باز می کند URL
+2. پس از تأیید، Google سعی میکند به `localhost` تغییر مسیر دهد (که در سرور راه دور با مشکل مواجه میشود)
+3. **URL** را از نوار آدرس مرورگر خود کپی کنید (حتی اگر صفحه بارگیری نشود)
+4. آن URL را در فیلد نشان داده شده در مدال اتصال OmniRoute قرار دهید
+5. روی **"اتصال"** کلیک کنید
-> This works because the authorization code in the URL is valid regardless of whether the redirect page loaded.
+> این کار به این دلیل کار می کند که کد مجوز در URL بدون توجه به بارگیری صفحه تغییر مسیر معتبر است.
---
-## 🛠️ Tech Stack
+## 🛠️ پشته فناوری
Click to expand tech stack details
-- **Runtime**: Node.js 18–22 LTS (⚠️ Node.js 24+ is **not supported** — `better-sqlite3` native binaries are incompatible)
-- **Language**: TypeScript 5.9 — **100% TypeScript** across `src/` and `open-sse/` (zero `any` in core modules since v2.0)
-- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
-- **Database**: better-sqlite3 (SQLite) + LowDB (JSON legacy) — domain state, proxy logs, MCP audit, routing decisions, memory, skills
-- **Schemas**: Zod (MCP tool I/O validation, API contracts)
-- **Protocols**: MCP (stdio/HTTP) + A2A v0.3 (JSON-RPC 2.0 + SSE)
-- **Streaming**: Server-Sent Events (SSE)
-- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys + MCP Scoped Authorization
-- **Testing**: Node.js test runner + Vitest (900+ tests including unit, integration, E2E)
-- **CI/CD**: GitHub Actions (auto npm publish + Docker Hub on release)
-- **Website**: [omniroute.online](https://omniroute.online)
-- **Package**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
+- **زمان اجرا **: Node.js 18–22 LTS (
+- **زبان**: TypeScript 5.9 — **100% TypeScript** در `src/` و `open-sse/` (صفر `any` در ماژول های اصلی از نسخه 2.0)
+- **فریم ورک**: Next.js 16 + React 19 + Tailwind CSS 4
+- **پایگاه داده**: بهتر-sqlite3 (SQLite) + LowDB (میراث JSON) - وضعیت دامنه، لاگ های پروکسی، ممیزی MCP، تصمیمات مسیریابی، حافظه، مهارت ها
+- **طرحواره**: Zod (تأیید اعتبار I/O ابزار MCP، قراردادهای API)
+- **پروتکل ها**: MCP (stdio/HTTP) + A2A نسخه 0.3 (JSON-RPC 2.0 + SSE)
+- **جریان**: رویدادهای ارسال شده از سرور (SSE)
+- **Auth**: OAuth 2.0 (PKCE) + کلیدهای JWT + API + مجوز محدوده MCP
+- **تست**: Node.js تست دونده + Vitest (900+ تست شامل واحد، ادغام، E2E)
+- **CI/CD**: اقدامات GitHub (انتشار خودکار npm + مرکز Docker در زمان انتشار)
+- **وب سایت**: [omniroute.online](https://omniroute.online)
+- **بسته**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
- **Docker**: [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
-- **Resilience**: Circuit breaker, exponential backoff, anti-thundering herd, TLS spoofing, auto-combo self-healing
+- **ارتعاش**: قطع کننده مدار، عقب نشینی نمایی، گله ضد رعد و برق، جعل TLS، خودترمیمی خودکار ترکیبی
---
-## Documentación
+## مستندات
-| Document | Description |
+| سند | توضیحات |
| --------------------------------------------------------------------- | --------------------------------------------------- |
-| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
-| [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples |
-| [MCP Server](open-sse/mcp-server/README.md) | 107 MCP tools, IDE configs, Python/TS/Go clients |
-| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
-| [Auto-Combo Engine](docs/auto-combo.md) | 13-factor scoring, mode packs, self-healing |
-| [Context Relay](docs/features/context-relay.md) | Session handoff strategy for account rotation |
-| [Troubleshooting](docs/guides/TROUBLESHOOTING.md) | Common problems and solutions |
-| [Architecture](docs/architecture/ARCHITECTURE.md) | System architecture and internals |
-| [Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
-| [Uninstall Guide](docs/guides/UNINSTALL.md) | Clean removal for all install methods |
-| [Environment Config](docs/reference/ENVIRONMENT.md) | Complete `.env` variables and references |
-| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
-| [OpenAPI Spec](docs/reference/openapi.yaml) | OpenAPI 3.0 specification |
-| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
-| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
-| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
-| [Release Checklist](docs/ops/RELEASE_CHECKLIST.md) | Pre-release validation steps |
+| [راهنمای کاربر](docs/guides/USER_GUIDE.md) | ارائه دهندگان، ترکیب، یکپارچه سازی CLI، استقرار |
+| [مرجع API](docs/reference/API_REFERENCE.md) | تمام نقاط پایانی با مثال |
+| [سرور MCP](open-sse/mcp-server/README.md) | 107 ابزار MCP، تنظیمات IDE، کلاینت های Python/TS/Go |
+| [سرور A2A](src/lib/a2a/README.md) | پروتکل JSON-RPC 2.0، مهارت ها، جریان، وظیفه mgmt |
+| [موتور ترکیبی خودکار](docs/auto-combo.md) | امتیاز دهی 13 عاملی، بسته های حالت، خوددرمانی |
+| [رله زمینه](docs/features/context-relay.md) | استراتژی انتقال جلسه برای چرخش حساب |
+| [عیب یابی](docs/guides/TROUBLESHOOTING.md) | مشکلات و راه حل های رایج |
+| [معماری](docs/architecture/ARCHITECTURE.md) | معماری سیستم و قطعات داخلی |
+| [اسناد پایگاه کد](docs/architecture/CODEBASE_DOCUMENTATION.md) | راهنمای مبتدی مبتدی پایه کد |
+| [راهنمای حذف نصب](docs/guides/UNINSTALL.md) | حذف پاک برای همه روش های نصب |
+| [پیکربندی محیط](docs/reference/ENVIRONMENT.md) | تکمیل متغیرها و مراجع `.env` |
+| [مشارکت](CONTRIBUTING.md) | راه اندازی و دستورالعمل های توسعه |
+| [مشخصات OpenAPI](docs/reference/openapi.yaml) | مشخصات OpenAPI 3.0 |
+| [سیاست امنیتی](SECURITY.md) | گزارش آسیب پذیری و شیوه های امنیتی |
+| [استقرار VM](docs/ops/VM_DEPLOYMENT_GUIDE.md) | راهنمای کامل: راه اندازی VM + nginx + Cloudflare |
+| [گالری امکانات](docs/guides/FEATURES.md) | تور داشبورد بصری با اسکرین شات |
+| [چک لیست انتشار](docs/ops/RELEASE_CHECKLIST.md) | مراحل اعتبار سنجی پیش از انتشار |
---
-## 🗺️ Roadmap
+## 🗺️ نقشه راه
-OmniRoute has **218+ features planned** across multiple development phases. Here are the key areas:
+OmniRoute دارای **218+ ویژگی برنامه ریزی شده** در چندین فاز توسعه است. در اینجا مناطق کلیدی وجود دارد:
-| Category | Planned Features | Highlights |
+| دسته بندی | ویژگی های برنامه ریزی شده | نکات برجسته |
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- |
-| 🧠 **Routing & Intelligence** | 25+ | Lowest-latency routing, tag-based routing, quota preflight, quota-aware P2C, step-based combo routing |
-| 🔒 **Security & Compliance** | 20+ | SSRF hardening, credential cloaking, rate-limit per endpoint, management key scoping |
-| 📊 **Observability** | 15+ | OpenTelemetry integration, real-time quota monitoring, combo target health, cost tracking per model |
-| 🔄 **Provider Integrations** | 20+ | Dynamic model registry, connection cooldowns, multi-account Codex, Copilot quota parsing |
-| ⚡ **Performance** | 15+ | Dual cache layer, prompt cache, response cache, streaming keepalive, batch API |
-| 🌐 **Ecosystem** | 10+ | WebSocket API, config hot-reload, distributed config store, commercial mode |
+| 🧠 **مسیریابی و هوشمندی** | 25+ | مسیریابی با کمترین تأخیر، مسیریابی مبتنی بر برچسب، پیش از پرواز سهمیه، P2C آگاه از سهمیه، مسیریابی ترکیبی مبتنی بر گام |
+| 🔒 **امنیت و انطباق** | 20+ | سخت شدن SSRF، پوشاندن اعتبار، نرخ محدود در نقطه پایانی، محدوده کلید مدیریت |
+| 📊 **قابلیت مشاهده** | 15+ | ادغام OpenTelemetry، نظارت بر سهمیه در زمان واقعی، سلامت هدف ترکیبی، ردیابی هزینه در هر مدل |
+| 🔄 **یکپارچه سازی ارائه دهنده** | 20+ | رجیستری مدل پویا، خنک کننده اتصال، چند حساب Codex، تجزیه سهمیه Copilot |
+| ⚡ **عملکرد** | 15+ | لایه کش دوگانه، کش سریع، حافظه پنهان پاسخ، جریان نگهدارنده، دسته ای API |
+| 🌐 **اکوسیستم** | 10+ | WebSocket API، پیکربندی بارگذاری مجدد داغ، فروشگاه پیکربندی توزیع شده، حالت تجاری |
-### 🔜 Coming Soon
+### 🔜 به زودی
-- 🔗 **OpenCode Integration** — Native provider support for the OpenCode AI coding IDE
-- 🔗 **TRAE Integration** — Full support for the TRAE AI development framework
-- 📦 **Batch API** — Asynchronous batch processing for bulk requests
-- 🎯 **Tag-Based Routing** — Route requests based on custom tags and metadata
-- 💰 **Lowest-Cost Strategy** — Automatically select the cheapest available provider
+- 🔗 **ادغام OpenCode** — پشتیبانی ارائه دهنده بومی برای کدنویسی OpenCode AI IDE
+- 🔗 **ادغام TRAE** — پشتیبانی کامل از چارچوب توسعه TRAE AI
+- 📦 ** دسته ای API ** - پردازش دسته ای ناهمزمان برای درخواست های انبوه
+- 🎯 **مسیریابی مبتنی بر برچسب** - درخواست های مسیر بر اساس برچسب ها و ابرداده های سفارشی
+- 💰 **استراتژی کم هزینه** - ارزانترین ارائه دهنده موجود را به صورت خودکار انتخاب کنید
-> 📝 Full feature specifications available in [`docs/new-features/`](docs/new-features/) (217 detailed specs)
+> 📝 مشخصات کامل ویژگی موجود در [`docs/new-features/`](docs/new-features/) (217 مشخصات دقیق)
---
-## 👥 Contributors
+## 👥 مشارکت کنندگان
[](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
-### How to Contribute
+### نحوه مشارکت
-1. Fork the repository
-2. Create your feature branch (`git checkout -b feature/amazing-feature`)
-3. Commit your changes (`git commit -m 'Add amazing feature'`)
-4. Push to the branch (`git push origin feature/amazing-feature`)
-5. Open a Pull Request
+1. مخزن را چنگال کنید
+2. شاخه ویژگی خود را ایجاد کنید (`git checkout -b feature/amazing-feature`)
+3. تغییرات خود را انجام دهید (`git commit -m 'Add amazing feature'`)
+4. فشار به شاخه (`git push origin feature/amazing-feature`)
+5. یک Pull Request را باز کنید
-See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
+برای دستورالعمل های دقیق به [CONTRIBUTING.md](CONTRIBUTING.md) مراجعه کنید.
-### Releasing a New Version
+### انتشار یک نسخه جدید
```bash
# Create a release — npm publish happens automatically
@@ -2189,7 +2189,7 @@ gh release create v2.0.0 --title "v2.0.0" --generate-notes
---
-## 📊 Star History
+## 📊 تاریخچه ستاره
@@ -2209,15 +2209,15 @@ gh release create v2.0.0 --title "v2.0.0" --generate-notes
-## 🙏 Acknowledgments
+## 🙏 تشکر و قدردانی
-Special thanks to **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — the original Go implementation that inspired this JavaScript port.
+تشکر ویژه از **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** - پیاده سازی اصلی Go که الهام بخش این پورت جاوا اسکریپت است.
---
-## Licencia
+## مجوز
-MIT License - see [LICENSE](LICENSE) for details.
+مجوز MIT - برای جزئیات بیشتر به [LICENSE](LICENSE) مراجعه کنید.
---
diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt
index 1d98061631c5..c81ec3a85348 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 01fb1ec72b7a..974084ef142d 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 b20f38d80473..6db495b3d95f 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 3cabcd235f85..885f78df633d 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 d1dca10cc19e..617ce45e8c7b 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 e88a70b26ad1..67b0cfeba3df 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 fdd95262797e..fa74ab59976d 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 776304488c6f..71572bd5a43d 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 2e0784f344c6..c6f22ffc46cd 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 160add875cfc..db4f6a9cfda3 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 7e3f75466fa1..f021f83d5a30 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 6cd010d0a49c..52cf209a74f0 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 12129f663df0..cc3125f845fe 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 8c01bf3654c2..650ac31922ef 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 7fe980153e3d..bdd29b2d0121 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 32364510ee9d..7cb57aa53cd8 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 54595155ef39..73ea9e49ba4e 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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/docs/compression/COMPRESSION_ENGINES.md b/docs/i18n/pl/docs/compression/COMPRESSION_ENGINES.md
index b8488d68728e..ea266af05bfd 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 1d9cdb29f65f..535dadd53c75 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/pl/llm.txt b/docs/i18n/pl/llm.txt
index 18c1d5aeafd7..7d1d8be0f292 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 8dc803bdbe3f..59be9758e4dd 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 0a841f4d7fb8..1f4f205d44b4 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 4461a57b480d..485f971d1f7b 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 e4b51b687bb2..319111231a5f 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 3783940896f0..edb3477ef2fe 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 3e4eb1f5f47c..48847120cb8b 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 ec134c100a38..f90e0ec031ce 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 64848422b9b3..8f64c7aab8b7 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 ebb7d28a0c41..483d2f8a418b 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 dc0102552697..ac3a14103aa9 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 182af11a3ba6..d6e2fbd674b1 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 bd5997d66ab7..17e04f3ce166 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 6d04724edb9b..9291f526424f 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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 ff99a1050edc..0cc3e4e22e38 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/zh-CN/docs/ops/RELEASE_CHECKLIST.md
index d8cefd6497b0..afdbf398afe9 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-CN/llm.txt b/docs/i18n/zh-CN/llm.txt
index 925f9e61845e..28d5e6fe2348 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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/docs/ops/RELEASE_CHECKLIST.md b/docs/i18n/zh-TW/docs/ops/RELEASE_CHECKLIST.md
index bbadf0342365..c604668d197f 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/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt
index 1c95e2575523..ae8fd155b58a 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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 (341), 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
-- **341 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
-- **341-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/openapi.yaml b/docs/openapi.yaml
index 79cee7893bd8..fb255d3543dc 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -2069,6 +2069,39 @@ paths:
description: Created combo
/api/combos/{id}:
+ get:
+ tags: [Combos]
+ summary: Get combo by ID
+ parameters:
+ - $ref: "#/components/parameters/ResourceId"
+ responses:
+ "200":
+ description: Combo details
+ "404":
+ description: Combo not found
+ put:
+ tags: [Combos]
+ summary: Update combo
+ description: >-
+ Partial update: the body is merged onto the stored combo, so a field left out keeps
+ its current value. An array that IS sent replaces the stored one outright.
+ parameters:
+ - $ref: "#/components/parameters/ResourceId"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ responses:
+ "200":
+ description: Updated combo
+ "400":
+ description: Invalid body, or the resulting combo fails validation
+ "404":
+ description: Combo not found
+ "409":
+ description: Name already taken, or the combo is quota-share managed
patch:
tags: [Combos]
summary: Update combo
@@ -7329,12 +7362,18 @@ components:
BearerAuth:
type: http
scheme: bearer
- description: API key obtained from the OmniRoute dashboard
+ description: >
+ Two bearer families are accepted. Inference API keys (typically `sk-…`)
+ authorize `/v1/*`. Management routes also accept `oma_live_…` Access Tokens
+ (Settings → Access Tokens / `omniroute connect`) and API keys whose metadata
+ includes `manage` or `admin` scope. See docs/guides/MANAGEMENT-AUTH.md.
+ Bearer credentials are accepted on management routes that use this scheme;
+ they are not rejected solely for being Bearer.
ManagementSessionAuth:
type: apiKey
in: cookie
name: auth_token
- description: Dashboard management session cookie for protected management routes
+ description: Dashboard management session cookie (auth_token) for protected management routes. Distinct from Bearer Access Tokens and API keys. See docs/guides/MANAGEMENT-AUTH.md.
parameters:
ResourceId:
diff --git a/docs/ops/DATABASE_GUIDE.md b/docs/ops/DATABASE_GUIDE.md
index e0a0292a05f5..55d3771ce6f5 100644
--- a/docs/ops/DATABASE_GUIDE.md
+++ b/docs/ops/DATABASE_GUIDE.md
@@ -453,26 +453,47 @@ Run monthly during low-traffic windows. (WAL mode reduces the need, but doesn't
`src/lib/db/healthCheck.ts` provides **DB-level health diagnostics**:
-````bash
-GET /api/db/health
+Both verbs require authentication (`401` otherwise). `GET` diagnoses only; `POST` runs the
+same check with `autoRepair` enabled.
-Returns:
+```bash
+GET /api/db/health # diagnose
+POST /api/db/health # diagnose + repair
+```
+
+The response is the `DbHealthCheckResult` produced by `runDbHealthCheck()`
+(`src/lib/db/healthCheck.ts`):
```json
{
- "status": "healthy",
- "checks": {
- "writable": { "status": "pass" },
- "integrity": { "status": "pass", "result": "ok" },
- "foreign_keys": { "status": "pass", "violations": 0 },
- "orphaned_artifacts": { "status": "warn", "count": 12 },
- "table_sizes": {
- "usage_history": { "rows": 12345, "size_mb": 12.3 },
- "call_logs": { "rows": 567, "size_mb": 2.1 }
+ "isHealthy": false,
+ "issues": [
+ {
+ "type": "broken_reference",
+ "table": "domain_budgets",
+ "description": "Domain budgets referenced API keys that no longer exist.",
+ "count": 2
}
- }
+ ],
+ "repairedCount": 0,
+ "backupCreated": false,
+ "autoRepair": false,
+ "checkedAt": "2026-08-18T09:00:00.000Z",
+ "driver": { "name": "better-sqlite3", "degraded": false }
}
-````
+```
+
+| Field | Meaning |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `isHealthy` | `true` when `issues` is empty. `driver` never influences it. |
+| `issues[].type` | One of `integrity_check_failed`, `broken_reference`, `stale_snapshot`, `invalid_state`. |
+| `repairedCount` | Rows repaired during this run; always `0` when `autoRepair` is false. |
+| `backupCreated` | Whether a backup was taken before repairing. |
+| `checkedAt` | ISO timestamp shared by the run and by any repair note it writes. |
+| `driver.name` | SQLite driver serving the checked database. |
+| `driver.degraded` | `true` when writes are not durably backed by the database file — the `sql.js` WASM fallback (whole-file persistence) or an in-memory database. |
+
+The same payload is returned by the `omniroute_db_health_check` MCP tool.
Run `PRAGMA integrity_check` to detect corruption:
diff --git a/docs/ops/MONITORING_GUIDE.md b/docs/ops/MONITORING_GUIDE.md
index 82a66eeb499a..6543b95f064c 100644
--- a/docs/ops/MONITORING_GUIDE.md
+++ b/docs/ops/MONITORING_GUIDE.md
@@ -157,13 +157,13 @@ Response:
### Kubernetes probe recommendations
-OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHECK` targets `/api/monitoring/health` — that is **too heavy** for kubelet liveness intervals.
+OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHECK` targets lightweight `/healthz`. `/api/monitoring/health` is **too heavy** for kubelet liveness intervals.
| Probe | Recommended target | Notes |
| --- | --- | --- |
| **Startup** | HTTP `GET /healthz` with a long `failureThreshold` (or large `startPeriod`) | Cold start + SQLite migration can exceed a few seconds |
-| **Readiness** | HTTP `GET /healthz` | Remove endpoints while starting/stopping; still flaps if the loop is CPU-blocked |
-| **Liveness** | **TCP** on the main service port (`PORT`, default `20128`), **or** HTTP `/healthz` with soft thresholds | Do **not** kill the pod on short event-loop stalls; busy ≠ dead |
+| **Readiness** | HTTP `GET /healthz` | Lifecycle `ok` / `starting` / `stopping` (200 vs 503). Still flaps if the loop is CPU-blocked. A **200 in multiple seconds is not healthy** (#10303) — it means the event loop was starved before the 3-byte handler ran |
+| **Liveness** | HTTP `GET /livez`, **or TCP** on the main service port (`PORT`, default `20128`) | `/livez` is process-alive only (always 200 if the handler runs). It still shares the event loop — busy ≠ dead, and it does not detect event-loop starvation (#10303) any better than TCP does. Prefer **TCP** if HTTP probes time out under catalog/compression load; do **not** kill the pod on short event-loop stalls either way |
| **Deep health** | `GET /api/monitoring/health` from an external checker | Not for kubelet `livenessProbe` / tight `readinessProbe` |
Example shape (adjust thresholds to your cold-start and compression load):
@@ -186,17 +186,27 @@ readinessProbe:
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
- tcpSocket:
+ httpGet:
+ path: /livez
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
+ # Under event-loop stall HTTP /livez can still time out. TCP is the
+ # conservative alternative:
+ # tcpSocket:
+ # port: http
```
**Do not** point kubelet **liveness** at `/api/monitoring/health`. That path does real DB/monitoring work and will false-positive under load.
Related: [#10052](https://github.com/diegosouzapw/OmniRoute/issues/10052) (probes while the event loop is busy), [#9685](https://github.com/diegosouzapw/OmniRoute/issues/9685) / [#10055](https://github.com/diegosouzapw/OmniRoute/pull/10055) (catalog pricing hog), [#10117](https://github.com/diegosouzapw/OmniRoute/issues/10117) (compression token-count hog).
+
+### Optional request-path work (memory, skills, token refresh)
+
+Memory extraction, skills injection, and OAuth token refresh share the **main Node event loop** with `/healthz`. They are dashboard-toggle features (`memoryEnabled`, `skillsEnabled`), not a worker pool. See [Environment — event-loop cost](../reference/ENVIRONMENT.md#event-loop-cost-of-memory-skills-and-token-refresh-10349).
+
### Provider Health
> **No REST endpoint.** Provider health data is available via the MCP tool `observability_snapshot` or the dashboard `/dashboard/providers` page.
diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md
index b800d6fc22ae..dfa96d53eed2 100644
--- a/docs/ops/RELEASE_CHECKLIST.md
+++ b/docs/ops/RELEASE_CHECKLIST.md
@@ -66,6 +66,15 @@ as the default reflex (minutes, reversible); `npm unpublish` only inside the 72h
window and never as the first move. Docker: never rewrite a version tag — rollback is
repointing `latest` to the last good digest.
+**Docker Hub `latest` (required on every stable SemVer publish):** the
+`docker-publish` workflow must tag **both** `X.Y.Z` and, when
+`should-promote-latest.sh` agrees this is the highest stable SemVer, `:latest`
+with the **same digest**. After the job: Hub `latest` digest equals the new
+SemVer digest and `last_updated` moved. Do not leave `:latest` on an older
+build while release notes talk about fixes that only exist on git. Compose
+quickstarts use `:latest`; GitOps should keep pinning `X.Y.Z`. See
+[Docker release channels](../guides/DOCKER_GUIDE.md#release-channels) and #10317.
+
## Hotfix Fast-Lane (label `hotfix`)
A PR labeled `hotfix` skips the heavy CI matrix (9-shard E2E, coverage ratchet,
@@ -342,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/docs/ops/SQLITE_RUNTIME.md b/docs/ops/SQLITE_RUNTIME.md
index e2e6a412da8a..ab31996fb201 100644
--- a/docs/ops/SQLITE_RUNTIME.md
+++ b/docs/ops/SQLITE_RUNTIME.md
@@ -80,3 +80,17 @@ Implementation:
- `bin/cli/runtime/index.mjs` — startup orchestrator (`warmUpRuntimes()`)
- `scripts/postinstall.mjs` — npm post-install hook (non-fatal warm-up)
- `src/lib/db/core.ts` — `ensureDbInitialized()` / `getDriverInfo()` exports
+
+## Single-writer topology (HA unsupported)
+
+The driver fallback chain above still runs in **one process**. Default SQLite
+OmniRoute is a **single writer**:
+
+- Do not attach two OmniRoute replicas to the same `storage.sqlite` file.
+- A container restart, Recreate deploy, OOM kill, or HEALTHCHECK restart drops
+ every in-flight SSE session. There is no session drain on the stock path.
+- Orchestrator liveness that treats a slow `/healthz` as dead will kill the only
+ replica. Prefer TCP liveness + HTTP `/healthz` readiness. See
+ [Docker Guide — availability](../guides/DOCKER_GUIDE.md#availability-default-sqlite-is-single-replica)
+ and [Kubernetes probe recommendations](./MONITORING_GUIDE.md#kubernetes-probe-recommendations).
+
diff --git a/docs/providers/CURSOR-API-KEY-AND-CLI.md b/docs/providers/CURSOR-API-KEY-AND-CLI.md
new file mode 100644
index 000000000000..53c0a85dfd62
--- /dev/null
+++ b/docs/providers/CURSOR-API-KEY-AND-CLI.md
@@ -0,0 +1,123 @@
+---
+title: "Cursor API provider and the Cursor CLI passthrough"
+version: 3.8.50
+lastUpdated: 2026-08-19
+---
+
+# Cursor API provider and the Cursor CLI passthrough
+
+Two ways to put Cursor behind OmniRoute without an IDE session:
+
+1. **`cursor-api` provider** (card "Cursor API", alias `cua`): an API-key
+ provider that holds a Cursor user API key (`crsr_…`, generated at
+ `https://cursor.com/dashboard/api`). Any OmniRoute client then reaches
+ Cursor models through `/v1/chat/completions` as `cursor-api/` or
+ `cua/`, with the usual quota, fallback and logging layers. The IDE
+ provider (`cursor`, OAuth/IDE session) is unchanged.
+2. **Cursor CLI passthrough**: point the Cursor CLI (`agent`) at OmniRoute so
+ every RPC the CLI makes is authenticated with an OmniRoute API key, forwarded
+ to Cursor with a `cursor-api` connection's credential, and recorded in the
+ Logs page.
+
+## Why the key is exchanged
+
+`api2.cursor.sh` rejects a raw `crsr_…` key as a Bearer token (401). The Cursor
+CLI first POSTs the key to `/auth/exchange_user_api_key` and receives a session
+JWT that expires after one hour; the returned `refreshToken` carries the same
+`exp`, so refreshing means re-exchanging the key.
+`open-sse/services/cursorApiKeyAuth.ts` does that exchange, caches one session
+token per key, re-exchanges five minutes before expiry and drops the cached
+token when Cursor answers 401. `CursorExecutor` calls it right before opening
+the upstream stream for `cursor-api` connections.
+
+## The `cursor-api` provider
+
+Registry: `open-sse/config/providers/registry/cursor/index.ts`
+(`cursor_apiProvider`, `authType: "apikey"`, same `format`, `baseUrl` and
+`models` as `cursor`). Catalog card:
+`src/shared/constants/providers/apikey/specialty-media.ts`. Executor map:
+`open-sse/executors/index.ts` (`"cursor-api"` / `cua` →
+`new CursorExecutor("cursor-api")`).
+
+Dashboard: Providers → Cursor API → Add API key.
+
+REST:
+
+```bash
+curl -sS -X POST http://localhost:20128/api/providers \
+ -H "Content-Type: application/json" \
+ -d '{"provider":"cursor-api","name":"cursor-api-key","apiKey":"crsr_…","priority":1}'
+```
+
+Then:
+
+```bash
+curl -sS http://localhost:20128/v1/chat/completions \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{"model":"cursor-api/auto","messages":[{"role":"user","content":"say PONG"}]}'
+```
+
+Notes:
+
+- Model listing for `cursor-api` comes from the static Cursor registry (the
+ same list the IDE provider falls back to); no `cursor-agent` install is
+ needed on the OmniRoute host.
+- `POST /api/providers/{id}/refresh-cursor` is for the `cursor` IDE provider
+ only; `cursor-api` connections have no IDE session to renew.
+
+## Cursor CLI passthrough
+
+Route: `src/app/api/cursor-cli/[...path]/route.ts` →
+`open-sse/handlers/cursorCliProxy.ts`. The prefix `/api/cursor-cli/` is
+registered in `src/shared/constants/publicApiRoutes.ts` because the handler
+enforces its own authentication:
+
+| Path | Auth expected from the CLI | What OmniRoute does |
+| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `POST /auth/exchange_user_api_key` | `Bearer ` | Validates the key, mints a 1h HS256 JWT (signed with `JWT_SECRET`) and returns it |
+| every other path (`/aiserver.v1.*`, `/agent.v1.AgentService/RunSSE`, `/aiserver.v1.BidiService/BidiAppend`, `/v1/traces`, …) | `Bearer ` | Verifies issuer/audience/expiry, picks an active `cursor-api` connection, swaps the Authorization header for the exchanged Cursor token and streams the reply back |
+
+The CLI decodes `exp` from whatever token it receives, so handing it an opaque
+token makes it re-exchange before almost every request; the minted JWT avoids
+that. A 401 from OmniRoute makes the CLI exchange again.
+
+### Setup
+
+1. Create an OmniRoute API key (Dashboard → API keys) and a `cursor-api`
+ connection.
+2. Tell the CLI to use HTTP/1.1 for the agent stream. In
+ `~/.cursor/cli-config.json`:
+
+ ```json
+ { "network": { "useHttp1ForAgent": true } }
+ ```
+
+ Without this the CLI opens the agent turn over HTTP/2 to a separately
+ configured agent host and only the control-plane RPCs go through the
+ endpoint.
+
+3. Run the CLI against OmniRoute:
+
+ ```bash
+ export CURSOR_API_ENDPOINT=http://localhost:20128/api/cursor-cli
+ export CURSOR_API_KEY=
+ agent -p --trust "Reply with exactly OK"
+ ```
+
+Every hop lands in Logs as provider `cursor-api`, request type `cursor-cli`,
+path `/api/cursor-cli/`, attributed to the OmniRoute API key and the
+connection that served it.
+
+### Failure modes
+
+| Situation | Response to the CLI |
+| ------------------------------------------------ | --------------------------------------------- |
+| Unknown OmniRoute key and `REQUIRE_API_KEY=true` | 401 `unauthenticated` on exchange |
+| `REQUIRE_API_KEY=false` | anonymous session (mirrors `/v1/*` behaviour) |
+| Expired / foreign / tampered session JWT | 401, the CLI re-exchanges |
+| OmniRoute API key revoked after exchange | 401 on the next RPC |
+| No active `cursor-api` connection | 503 `unavailable` |
+| Cursor rejects the connection's key | 401 `unauthenticated`, cached session dropped |
+| Upstream unreachable | 502 `unavailable` (sanitized message) |
+| `JWT_SECRET` unset | 503 on exchange |
diff --git a/docs/providers/CURSOR_IMAGE.md b/docs/providers/CURSOR_IMAGE.md
new file mode 100644
index 000000000000..a620c4de7945
--- /dev/null
+++ b/docs/providers/CURSOR_IMAGE.md
@@ -0,0 +1,75 @@
+---
+title: "Cursor Image Generation"
+version: 3.8.49
+lastUpdated: 2026-07-23
+---
+
+# Cursor Image Generation
+
+OmniRoute exposes Cursor plan **image generation** on `POST /v1/images/generations` through the same provider id as chat: `cursor` (alias `cu`).
+
+| Field | Value |
+|-------|--------|
+| `IMAGE_PROVIDERS` id | `cursor` |
+| Format | `cursor-agent-image` |
+| Auth | Same OAuth / API-key connection as chat (`provider_connections.provider = "cursor"`) |
+| Models | `cursor/auto`, `cursor/composer-2`, `cursor/composer-2.5` |
+
+## Why the Agent CLI
+
+Cursor chat in OmniRoute uses `agent.v1.AgentService/Run` (protobuf). That path **rejects** built-in client tools (shell, write, …). Image generation is a Cursor-native tool executed by the **`agent` CLI** against the seat. The image handler therefore spawns `agent` with a locked prompt and a per-request temp workspace (same shape as community seat bridges), then returns OpenAI-compatible `b64_json`.
+
+## Access restriction (Hard Rules #15 + #17)
+
+This is the only `IMAGE_PROVIDERS` format that spawns a child process (the `agent`
+binary). Because `POST /v1/images/generations` is shared by ~40 other, non-spawning
+image providers that remote callers legitimately use, the whole route is **not**
+classified `LOCAL_ONLY` — instead `handleCursorAgentImageGeneration` enforces its own
+gate using the trusted `AUTHZ_HEADER_PEER_LOCALITY` verdict the authz pipeline stamps
+on every request (from the real TCP peer, never the spoofable `Host` header): only
+`loopback` and `lan` callers may reach the spawn; everything else (including a leaked
+API key replayed over a public tunnel) gets `403` before any credential lookup or
+process spawn happens. See `src/server/authz/policies/management.ts` for the same
+policy applied to the rest of the `LOCAL_ONLY` tier.
+
+## Concurrency gate is module-level (single-instance limitation)
+
+`CURSOR_IMG_MAX_CONCURRENT` is enforced by an in-memory counter/queue scoped to the
+Node module instance (`open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts`).
+It correctly limits concurrent `agent` spawns within one OmniRoute process, but does
+**not** coordinate across multiple processes/instances sharing the same Cursor seat
+(e.g. a multi-replica deployment) — each instance enforces its own independent limit.
+For a single-instance deployment (the default) this is exact; horizontally scaled
+deployments should keep `CURSOR_IMG_MAX_CONCURRENT` conservative per instance or route
+Cursor image traffic to a single instance.
+
+## Requirements
+
+1. A connected Cursor account in the dashboard (OAuth or `crsr_…` API key).
+2. The Cursor Agent binary available to the OmniRoute process:
+ - env `CURSOR_AGENT_BIN=/path/to/agent`, or
+ - `~/.local/bin/agent`, or
+ - `providerSpecificData.agentBin` on the Cursor connection.
+
+Optional tuning:
+
+| Env | Default | Meaning |
+|-----|---------|---------|
+| `CURSOR_IMG_TIMEOUT_MS` | `210000` | Per-image wall clock |
+| `CURSOR_IMG_MAX_CONCURRENT` | `2` | Shared-seat concurrency gate |
+| `CURSOR_IMG_MODEL` | (request model / `auto`) | Override CLI `--model` |
+
+## Example
+
+```bash
+curl -sS https:///v1/images/generations \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{"model":"cursor/auto","prompt":"a lantern in fog","size":"1024x1024"}'
+```
+
+Generation typically takes 1–2 minutes. Prefer an internal network path; edge proxies with ~100s timeouts will fail.
+
+## LiteLLM
+
+Register an image model with `mode: image_generation`, `api_base: http://omniroute:20128/v1`, and `model: openai/cursor/auto` (or bare `cursor/auto` depending on your LiteLLM version).
diff --git a/docs/providers/ZED-DOCKER.md b/docs/providers/ZED-DOCKER.md
index 21b096b48c39..e3519ea70cf8 100644
--- a/docs/providers/ZED-DOCKER.md
+++ b/docs/providers/ZED-DOCKER.md
@@ -103,7 +103,7 @@ The manual import endpoint can also be called directly:
```
POST /api/providers/zed/manual-import
Content-Type: application/json
-Authorization: Bearer
+Authorization: Bearer
{
"provider": "openai",
diff --git a/docs/providers/meta.json b/docs/providers/meta.json
index 82eec7eca764..fa6485dd570e 100644
--- a/docs/providers/meta.json
+++ b/docs/providers/meta.json
@@ -7,6 +7,7 @@
"CHATGPT_WEB",
"AGENTROUTER",
"ZED-DOCKER",
- "CURSOR-DOCKER"
+ "CURSOR-DOCKER",
+ "CURSOR-API-KEY-AND-CLI"
]
}
diff --git a/docs/reference/API_REFERENCE.md b/docs/reference/API_REFERENCE.md
index b2f5983b78c3..359ae4750eca 100644
--- a/docs/reference/API_REFERENCE.md
+++ b/docs/reference/API_REFERENCE.md
@@ -703,6 +703,10 @@ X-OmniRoute-No-Cache: true
## Dashboard & Management
+Management routes (`/api/*` except public auth/login) are **not** authorized by
+ordinary inference API keys. Credential families, scopes, and curl examples:
+[Management Authentication](../guides/MANAGEMENT-AUTH.md).
+
### Authentication
| Endpoint | Method | Description |
@@ -1668,9 +1672,14 @@ See [Security > Guardrails](../security/GUARDRAILS.md) for full details.
## Authentication
+See [Management Authentication](../guides/MANAGEMENT-AUTH.md) for the four
+credential families (dashboard session, local CLI token, `oma_live_…` Access
+Token, manage-scoped API key) and how they differ from inference keys.
+
- Dashboard routes (`/dashboard/*`) use `auth_token` cookie
- Login uses saved password hash; fallback to `INITIAL_PASSWORD`
- `requireLogin` toggleable via `/api/settings/require-login`
- `/v1/*` routes optionally require Bearer API key when `REQUIRE_API_KEY=true`
+- "management token" / "management-scoped API key" in this reference means one of the families in that guide — not an undefined extra secret type
> **Breaking change (v3.8.0)** — `/api/v1/agents/tasks/*` and the cooldown management endpoints now require **management auth** (dashboard `auth_token` cookie or a management-scoped API key). Clients that previously called these routes unauthenticated will receive `401 Unauthorized`. See commit `588a0333` (`fix(auth): require management auth for agent and cooldown APIs`).
diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md
index ccb6a6981fa9..401d7f6a305a 100644
--- a/docs/reference/ENVIRONMENT.md
+++ b/docs/reference/ENVIRONMENT.md
@@ -91,10 +91,11 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_DATA_DIR` | _(unset)_ | `open-sse/executors/promptql/threadSticky.ts` | **Fallback alias** for `DATA_DIR`, checked only when `DATA_DIR` is unset. Used to locate the PromptQL executor's on-disk thread-sticky session cache (`/promptql-thread-sessions.json`); if neither var is set, the cache stays in-memory only (not persisted across restarts). |
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
-| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |
+| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips automatic + pre-write SQLite file backups (startup, models.dev pricing save/clear, settings writes). Manual and pre-restore backups still run. Non-manual backups are also **throttled to at most once per 60 minutes** so hourly models.dev sync does not copy the whole DB on every pricing write. Dashboard **Settings → Storage** can disable auto-backup independently. |
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
+| `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS` | `21600000` (6h) | `src/lib/db/core.ts` | Override the periodic `wal_checkpoint(TRUNCATE)` interval (ms). Auto-checkpoint never shrinks the WAL file itself, and a long-running server never closes its DB. `0` disables. |
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts`, `src/lib/db/healthCheck.ts` | Set to `1` to skip the DB healthcheck entirely on startup. Useful for short-lived tasks and integration tests. |
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | Set to `1` to force the DB healthcheck loop on, even when it would normally be skipped (e.g., short-lived tasks). |
| `OMNIROUTE_SKIP_POSTINSTALL` | `0` | `scripts/postinstall.mjs` | Set to `1` to skip the native-runtime warm-up during `npm install`. Useful in CI/headless installs where sqlite is already built. |
@@ -152,6 +153,8 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. |
| `OMNIROUTE_USE_TURBOPACK` | `1` (Turbopack — code default) | `package.json` / Next.js 16 | Turbopack is the default bundler for `npm run dev` and `npm run build` (2-3× faster builds, benchmarked). Set to `0` to fall back to webpack on Windows, when running into native binding / bundler-compat incompatibilities, **or on RAM-constrained machines** — Turbopack production builds on this Next.js version line (16.2.x) are known upstream to peak far higher in memory than webpack on large module graphs (Next 16.3's Turbopack memory-eviction fix is not yet stable); webpack fallback peaks much lower. See #6409. |
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | _(unset)_ | `src/lib/db/core.ts` / `src/lib/db/healthCheck.ts` | Set to `1` to skip the SQLite integrity health check on startup. Useful for faster boot on large databases. |
+| `NOTIFY_SOCKET` | _(unset)_ | systemd (sd_notify protocol) | Set by systemd when the process runs under a service unit with sd_notify integration; OmniRoute reads it (see `OMNIROUTE_DISABLE_SD_NOTIFY`) to send READY/WATCHDOG notifications. Never set by the user. |
+| `OMNIROUTE_DISABLE_SD_NOTIFY` | _(unset)_ | `scripts/dev/systemd-notify.mjs` | Set to `1` to disable systemd sd_notify (Type=notify / WatchdogSec=) even when running under a systemd unit. The notifier is a no-op outside systemd regardless. |
| `CREDENTIAL_HEALTH_CHECK_INTERVAL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/scheduler.ts` | Interval (ms) for the background credential health check scheduler. Minimum: 10000 (10s). |
| `CREDENTIAL_HEALTH_CACHE_TTL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/cache.ts` | TTL (ms) for cached credential health status. |
| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. Search providers (SEARCH_VALIDATOR_CONFIGS in `src/lib/providers/validation/searchProviders.ts`, e.g. `tavily-search`) are always excluded from the sweep — their "validation" is a real billed upstream query, so they are never health-checked on a timer (#9970). |
@@ -279,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. |
---
@@ -346,6 +350,7 @@ Route upstream LLM provider calls through an HTTP or SOCKS5 proxy for egress con
| `HTTP_PROXY` | _(unset)_ | Node.js standard | HTTP proxy for upstream calls. |
| `HTTPS_PROXY` | _(unset)_ | Node.js standard | HTTPS proxy for upstream calls. |
| `ALL_PROXY` | _(unset)_ | Node.js standard | Universal proxy (supports `socks5://`). |
+| `OMNIROUTE_PROXY_ECHO_URL` | _(unset)_ | `src/lib/proxyEchoTarget.ts` | Pins the echo-IP target used by proxy egress probes to a single URL. Unset, the probe tries `api64.ipify.org` then `api4.ipify.org` so IPv4-only tunnels are not reported dead (#9694). |
| `NO_PROXY` | _(unset)_ | Node.js standard | Comma-separated hostnames/IPs to bypass the proxy. |
| `OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS` | `32` | `open-sse/utils/proxyDispatcher.ts` | Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher. Long-lived SSE streams such as Codex `/v1/responses` need more than one connection when several requests share the same account-level proxy. Values above `256` are capped. |
| `SOCKS_HANDSHAKE_TIMEOUT_MS` | `10000` | `open-sse/utils/socksConnectorWithFamily.ts` | SOCKS5 handshake (connect) timeout in ms. Raise it when a single residential gateway host is hit by high concurrency (e.g. 100 simultaneous requests) — the real handshake can exceed 10s under a saturated pool even though the proxy is reachable, which otherwise surfaces as a false `[Proxy Fast-Fail] Proxy unreachable`. Capped at `120000`. |
@@ -500,8 +505,10 @@ 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/server-init.ts` | Provider rate-limit and quota polling interval. |
+| `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). |
| `OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS` | `250` | `open-sse/services/quotaFetchThrottle.ts` | Min interval (ms) between consecutive upstream quota fetches on the per-request preflight/monitor path; spaces concurrent network calls so many accounts on one IP don't burst the upstream. Wired into the Codex (`/wham/usage`), DeepSeek, Bailian (both fetch sites), OpenCode, and Crof quota fetchers (#6009, #6911). The generic `usage.ts::getUsageForProvider` dispatch path (github/glm/minimax/nanogpt/xai/etc.) is not yet covered — tracked separately. Cache hits unaffected. `0` disables; clamped `0..5000`. |
| `PROVIDER_LIMITS_POST_USAGE_REFRESH_DELAY_MS` | `5000` | `src/lib/usage/providerLimits.ts` | Delay (ms) before refreshing provider limits after a real usage event, giving the upstream quota API time to register consumption. |
@@ -728,6 +735,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. |
@@ -751,6 +759,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. |
@@ -840,7 +849,7 @@ The logging system writes to both stdout and rotated log files. All configuratio
| Variable | Default | Description |
| -------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `OMNIROUTE_MEMORY_MB` | _auto_ | Runtime V8 heap limit (MB). When unset, calibrated dynamically (~35% of system RAM, clamped to `[512, 4096]`); `512` is only the floor when total memory can't be read. Set explicitly to override. Docker standalone and `omniroute serve` use it to set `--max-old-space-size`. |
+| `OMNIROUTE_MEMORY_MB` | _auto_ | **Recommended** Docker/standalone V8 heap limit (MB). When unset, calibrated dynamically (~35% of system RAM, clamped to `[512, 4096]`); `512` is only the floor when total memory can't be read. On `run-standalone.mjs` (Docker CMD), an **explicit** value is appended as `--max-old-space-size` and **wins** over a conflicting NODE_OPTIONS heap flag (V8 last-flag). `omniroute serve` still prefers an existing NODE_OPTIONS heap (#5238). Do not set both to different numbers — the process logs a warn naming both values and the winner. |
| `PROMPT_CACHE_MAX_SIZE` | `50` | Max cached system prompt entries. |
| `PROMPT_CACHE_MAX_BYTES` | `2097152` (2 MB) | Max total prompt cache size. |
| `PROMPT_CACHE_TTL_MS` | `300000` (5 min) | Prompt cache entry TTL. |
@@ -859,6 +868,19 @@ The logging system writes to both stdout and rotated log files. All configuratio
### Memory Engine (plan 21)
+### Event-loop cost of memory, skills, and token refresh (#10349)
+
+OmniRoute is a **single Node process**. Memory extraction/retrieval, skills injection, and provider token refresh run on that **same event loop** as `GET /healthz` and the dashboard. They are not a worker thread.
+
+| Work | Code | Default | Operator control |
+| --- | --- | --- | --- |
+| Memory extraction / retrieval | `src/lib/memory/` | Dashboard **memoryEnabled** (default on) | Turn off **Settings → Memory**. There is no separate env kill switch beyond disabling the feature in settings. |
+| Skills injection | `src/lib/skills/injection.ts` | Dashboard **skillsEnabled** (default on) | Turn off **Settings → Memory/Skills** (`skillsEnabled`). Sandbox knobs below only bound execution after injection is already on. |
+| Token refresh | `src/sse/services/tokenRefresh.ts` | On for connected OAuth/web providers | Disconnect the provider or let tokens stay valid; there is no `TOKEN_REFRESH=0` env today. |
+
+If `/healthz` is slow on a quiet box, disable memory + skills first, then check catalog/compression load (#10303, #9685). These features yield at `await` points but still compete for the one thread.
+
+
Embedding layer, vector store and reranking knobs for the persistent memory subsystem (`src/lib/memory/`).
| Variable | Default | Description |
@@ -967,7 +989,7 @@ desktop install.
| Variable | Default | Source File | Description |
| ----------------------------------- | ------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `MODELS_DEV_SYNC_ENABLED` | _(unset)_ | `src/lib/modelsDevSync.ts` | Hard override for models.dev pricing sync. Unset = honor Settings > AI (`modelsDevSyncEnabled`). `0`/`false`/`off`/`no` **wins over the DB** and skips both periodic sync and `getModelsDevPricing()` SQL/JSON scans (recovery when the dashboard is wedged on the same event loop). `1`/`true`/`on`/`yes` forces sync on. |
+| `MODELS_DEV_SYNC_ENABLED` | _(unset)_ | `src/lib/modelsDevSync.ts` | Hard override for models.dev pricing sync. Unset = honor Settings > AI (`modelsDevSyncEnabled`). `0`/`false`/`off`/`no` **wins over the DB** and skips both periodic sync and `getModelsDevPricing()` SQL/JSON scans (recovery when the dashboard is wedged on the same event loop). `1`/`true`/`on`/`yes` forces sync on. Pricing save/clear still call `backupDbFile("pre-write")`, which is no-op under the 60-minute throttle or `DISABLE_SQLITE_AUTO_BACKUP`. |
| `MODELS_DEV_SYNC_INTERVAL` | `86400` (24h) | `src/lib/modelsDevSync.ts` | Development-time model catalog sync interval in seconds. |
| `CONTEXT_WINDOW_RECONCILE_INTERVAL` | `86400` (24h) | `src/lib/contextWindowResolver.ts` | Interval (seconds) for the self-correcting context-window reconciler (5004): pins provider-declared windows from `/models` discovery as `auto:discovery` overrides when they diverge from the catalog. Set to `0` to disable. Reuses already-synced data (no new fetch); never overwrites `manual` overrides. |
@@ -1029,7 +1051,10 @@ Anthropic-compatible provider instead.
| `PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS` | `2000` | `src/lib/proxyHealth.ts` | Cache TTL for failed proxy health probes. Keep this shorter than `PROXY_HEALTH_CACHE_TTL_MS` so transient proxy timeouts under high concurrency retry quickly without disabling fast-fail for truly dead proxies. |
| `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. |
| `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). |
-| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
+| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/probeTarget.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
+| `PROXY_HEALTH_TEST_CONCURRENCY` | `10` | `src/lib/proxyHealth/probeTarget.ts` | Probes started at once per batch, shared by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Floored at 1 and capped at 50. |
+| `PROXY_HEALTH_TEST_STAGGER_MS` | `100` | `src/lib/proxyHealth/probeTarget.ts` | Delay in ms between two probe departures inside a batch. Without it the whole batch leaves at the same moment and a shared egress IP can trip a rate-limited target. Set to `0` to disable the spacing; capped at 5000. |
+| `PROXY_HEALTH_USE_PROVIDER_TARGET` | `true` | `src/lib/proxyHealth/providerProbeTarget.ts` | Set "false" to stop probing the real host of a proxy's assigned provider (`GET /models`, no API key) and always use `PROXY_HEALTH_TEST_URL` instead. |
| `PROXY_HEALTH_AUTO_DEACTIVATE` | `false` | `src/lib/proxyHealth/statusPolicy.ts` | When `false` (default), automated reachability probes (the scheduler + the `/api/settings/proxies/auto-test` "Test All" button) are **read-only** and never write a proxy's status — only the operator sets active/inactive, so a flaky probe can't strand an assigned proxy (#6246). Set `true` to restore the legacy test-and-set behaviour. |
| `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. |
| `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). |
@@ -1094,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/docs/reference/FEATURE_FLAGS.md b/docs/reference/FEATURE_FLAGS.md
index 45eededb2830..8b46db649b78 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/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md
index 13869aa651d1..30eac06ad56c 100644
--- a/docs/reference/PROVIDER_REFERENCE.md
+++ b/docs/reference/PROVIDER_REFERENCE.md
@@ -1,16 +1,16 @@
---
title: "Provider Reference"
version: 3.8.50
-lastUpdated: 2026-08-18
+lastUpdated: 2026-08-20
---
# Provider Reference
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
> Regenerate with: `npm run gen:provider-reference`
-> **Last generated:** 2026-08-18
+> **Last generated:** 2026-08-20
-Total providers: **341**. See category breakdown below.
+Total providers: **346**. See category breakdown below.
## Categories
@@ -62,8 +62,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `clinepass` | `cp` | ClinePass | OAuth | [link](https://cline.bot/cline-pass) | ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow. |
| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. |
| `codex` | `cx` | OpenAI Codex | OAuth | — | — |
-| `cursor` | `cu` | Cursor IDE | OAuth | — | — |
-| `devin-cli` | `dv` | Devin CLI | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
+| `cursor` | `cu` | Cursor IDE | OAuth, image | — | Image via Agent CLI (`CURSOR_AGENT_BIN`); same seat as chat |
+| `devin-cli` | `dv` | Devin CLI (Official) | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
| `devin-desktop` | — | Devin Desktop | OAuth | [link](https://devin.ai) | Paste an existing Devin API key from an authenticated Devin session. Key export availability and steps vary by Devin version and account. |
| `ghe-copilot` | `ghe-copilot` | GitHub Enterprise Copilot | OAuth | — | Enter your GHE instance URL (e.g., https://ghe.company.com) in provider settings, then authenticate via device flow. |
| `github` | `gh` | GitHub Copilot | OAuth | — | — |
@@ -91,7 +91,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `chatgpt-web-codex` | `cgpt-codex` | ChatGPT Web (Codex) | Web cookie | [link](https://chatgpt.com) | Paste the full ChatGPT Cookie header. OmniRoute verifies it in an isolated headless browser profile. | native |
| `claude-web` | `cw` | Claude Web | Web cookie | [link](https://claude.ai) | Paste your session cookie from claude.ai | none |
| `conol-web` | `cnl` | Conol (Unofficial/Experimental) | Web cookie | [link](https://conol.ai) | Use browser sign-in, or paste the full Cookie header from conol.ai. The __Secure-better-auth.session_token cookie is required. | — |
-| `copilot-m365-web` | `m365copilot` | Microsoft 365 Copilot (BizChat) | Web cookie | [link](https://m365.cloud.microsoft/chat) | Sign in at m365.cloud.microsoft/chat, then open DevTools → Network → filter 'WS' → click the Chathub WebSocket connection. Copy both the access_token query parameter AND the account-specific Chathub path segment from its request URL (wss://…/Chathub/?…&access_token=…). It is NOT an Authorization: Bearer header on an XHR/Fetch request. The token is short-lived; this is an unofficial integration. | — |
+| `copilot-m365-web` | `m365copilot` | Microsoft 365 Copilot (BizChat) | Web cookie | [link](https://m365.cloud.microsoft/chat) | Sign in at m365.cloud.microsoft/chat, then open DevTools → Network → filter 'WS' → click the Chathub WebSocket connection. Copy both the access_token query parameter AND the account-specific Chathub path segment from its request URL (wss://…/Chathub/?…&access_token=…). It is NOT an Authorization: Bearer header on an XHR/Fetch request. The token is short-lived; this is an unofficial integration. Optional: store a refresh_token in providerSpecificData.refreshToken (any Microsoft device-code/refresh flow for the substrate.office.com/sydney scopes) and OmniRoute pre-flight-refreshes the access token itself — otherwise re-capture after every ~75 min expiry. | — |
| `copilot-web` | `copilot` | Microsoft Copilot Web | Web cookie | [link](https://copilot.microsoft.com) | Paste the access_token from an authenticated copilot.microsoft.com request (DevTools → Network → Authorization), or export a HAR while logged in | — |
| `deepseek-web` | `ds-web` | DeepSeek Web | Web cookie | [link](https://chat.deepseek.com) | Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken | emulated |
| `doubao-web` | `db` | Dola Web (ByteDance) | Web cookie | [link](https://www.dola.com) | Paste the full Cookie header from www.dola.com. It should include sessionid, ttwid, and s_v_web_id. If s_v_web_id is unavailable, fp=verify_... from a chat/completion request URL can be used as a fallback. | — |
@@ -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) (228)
+## API Key Providers (paid / paid-with-free-credits) (231)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -169,6 +169,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `command-code` | `cmd` | Command Code | API key | [link](https://commandcode.ai/) | Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint. |
| `coze` | `coze` | Coze | API key | [link](https://coze.com) | Get API key at coze.com/open/api |
| `crof` | `crof` | CrofAI | API key | [link](https://crof.ai) | — |
+| `cursor-api` | `cua` | Cursor API | API key | [link](https://cursor.com/dashboard/api) | Paste a Cursor user API key (crsr_...) from cursor.com/dashboard/api. OmniRoute exchanges it for a session token on demand; no IDE or cursor-agent install is needed. Usage bills to the Cursor plan that owns the key. |
| `dahl` | `dahl` | Dahl | API key | [link](https://inference.dahl.global) | Click 'Add Account' to auto-generate a token, or add a manual API key. |
| `databricks` | `databricks` | Databricks | API key, enterprise | [link](https://www.databricks.com) | — |
| `datarobot` | `datarobot` | DataRobot | API key, enterprise | [link](https://docs.datarobot.com) | Use your DataRobot API token. Optional Base URL can be the account root (for LLM Gateway) or a deployment URL under /api/v2/deployments/. |
@@ -318,10 +319,12 @@ 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 |
| `together` | `together` | Together AI | API key, video | [link](https://www.together.ai) | — |
+| `token-kiosk` | `tk` | Token Kiosk | API key | [link](https://agent-router.gaib.ai) | Use your Token Kiosk API key in Authorization: Bearer . Fully OpenAI-compatible gateway. API base URL: https://agent-router.gaib.ai/v1. |
| `tokenreply` | `tokenreply` | TokenReply | API key, aggregator | [link](https://www.tokenreply.com) | Free-tagged models have model- and campaign-specific daily limits; no fixed global free quota is published. |
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer . Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
| `topaz` | `topaz` | Topaz | API key, image | [link](https://topazlabs.com) | — |
@@ -353,7 +356,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 |
|----|-------|------|------|---------|-------|
@@ -363,6 +366,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/docs/routing/AUTO-COMBO.md b/docs/routing/AUTO-COMBO.md
index 7a0b3871914e..bad9aa43dd28 100644
--- a/docs/routing/AUTO-COMBO.md
+++ b/docs/routing/AUTO-COMBO.md
@@ -159,6 +159,28 @@ enumerating every existing combo that shadows a model id, so operators who
hit this by accident (rather than intentionally, per #6940) have a signal.
The detection helper lives in `src/lib/combos/modelNameCollision.ts`.
+## Calling a Custom Combo From a Client
+
+Persisted combos (Settings → Combos) are only used when the client sends the combo's **exact name** in the `model` field — there is no fuzzy or partial matching of the combo name, and no `auto/` prefix involved. Resolution order (`getComboForModel()` in `src/sse/services/model.ts`):
+
+1. exact combo-name match (`model: "my-combo"`),
+2. `combo/` prefix (`model: "combo/my-combo"`),
+3. model→combo glob mappings (`/api/model-combo-mappings`).
+
+```bash
+curl -X POST http://localhost:20128/v1/chat/completions \
+ -H "Authorization: Bearer " \
+ -H "Content-Type: application/json" \
+ -d '{"model":"my-combo","messages":[{"role":"user","content":"Hello"}]}'
+```
+
+Two common pitfalls:
+
+- **`auto` does not use your combos.** `auto`/`auto/*` builds its own zero-config candidate pool and only consults persisted combos if a combo is literally named `auto` (not recommended). To route through a combo, send its exact name — not `auto`.
+- **`openrouter/auto` is a real paid OpenRouter product** ("Auto Best Available"), not an OmniRoute alias. It is the single static model entry of the OpenRouter registry (`open-sse/config/providers/registry/openrouter/index.ts`) and is billed separately. Use Settings → Routing → Hide paid models to exclude it from `auto` pools.
+
+See [#7992](https://github.com/diegosouzapw/OmniRoute/issues/7992) and [#7111](https://github.com/diegosouzapw/OmniRoute/issues/7111) for the original confusion this documents.
+
## How It Works (Persisted Auto-Combos)
The Auto-Combo Engine dynamically selects the best provider/model for each request using a **14-factor scoring function** (defined in `open-sse/services/autoCombo/scoring.ts` → `DEFAULT_WEIGHTS`). Weights form a normalized distribution (custom weights are renormalized by `normalizeScoringWeights()`).
diff --git a/docs/security/BAN_DETECTION.md b/docs/security/BAN_DETECTION.md
index 4f72fe770883..faf585267e15 100644
--- a/docs/security/BAN_DETECTION.md
+++ b/docs/security/BAN_DETECTION.md
@@ -38,7 +38,7 @@ this service has been disabled in this account (Antigravity)
> copy is `ACCOUNT_DEACTIVATED_SIGNALS` in `open-sse/services/accountFallback.ts`;
> treat the block above as a snapshot.
-Two adjacent, **separate** signal tables live in the same file and are *not* part
+Two adjacent, **separate** signal tables live in the same file and are _not_ part
of banned-keyword detection:
- `CREDITS_EXHAUSTED_SIGNALS` — billing/quota depleted (`insufficient_quota`,
@@ -70,7 +70,7 @@ upstream error response
narrower **`deactivated`** label (`isActive=false` when the connection has no
spare API keys) is written by the inline `chatCore.ts` path on **HTTP 401 / 403**
(classified via `classifyProviderError` → `ACCOUNT_DEACTIVATED`). Note the
- `markAccountUnavailable()` path writes a *different* terminal status —
+ `markAccountUnavailable()` path writes a _different_ terminal status —
**`expired`** — for the same `ACCOUNT_DEACTIVATED` signal (via
`resolveTerminalConnectionStatus`), so the same ban can surface as either
`deactivated` or `expired` depending on which path handled the response. (The
@@ -86,7 +86,7 @@ every failed upstream request flows through — it is **not** gated to
OAuth/subscription scrapers. The resulting terminal state is per **connection**,
not per provider.
-That said, the built-in *strings* are oriented toward subscription/OAuth
+That said, the built-in _strings_ are oriented toward subscription/OAuth
providers with real ban risk (ChatGPT Web, Claude Web, Codex, Muse Spark,
Antigravity). An API-key provider will only trip the detector if its error body
literally contains one of the substrings.
@@ -136,14 +136,62 @@ There is no separate "clear ban flag" button — recovery is re-test, re-auth, o
re-enable, matching the general terminal-state rule in
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md).
+## Probe isolation (model test-all)
+
+A **probe-origin failure** (model test-all / health-check dispatches executed
+inside `runAsProbe`) never removes a connection from the pool (#9817): it is
+**recorded for visibility** (`last_error`, `last_error_type`, `error_code`,
+`last_error_at`) but skips **every** routing mutation — cooldowns, terminal
+status (`banned` / `deactivated` / `credits_exhausted`), per-model lockouts,
+the provider circuit breaker, the 5-minute quota cache, OAuth token refresh
+and auto-disable. Only a real request-path failure deactivates. The recorded
+error is what makes a flagged account visible in the dashboard while it stays
+serving traffic.
+
+The single decision point is `shouldIsolateProbeFailures()`
+(`src/shared/utils/probeOrigin.ts`), consulted by **every** site that could
+mutate routing state from a probe-origin failure:
+
+- `markAccountUnavailable` (`auth.ts`) — record-only (`lastError` raw text,
+ `lastErrorType`, `errorCode`, `lastErrorAt`; deliberately **no**
+ `backoffLevel`, which would trigger the selection-time auto-decay and wipe
+ the record)
+- `maybeAutoDisableBannedAccount` — no auto-disable
+- `chatCore` — FORBIDDEN, ACCOUNT_DEACTIVATED, QUOTA_EXHAUSTED (record-only,
+ no terminal `credits_exhausted`), GEO_BLOCKED (no 24h exclusion),
+ MODEL_NOT_FOUND (no `lockModel`), the codex 429 account-rotation failover
+ (no `markCodexScopeRateLimited`, no persisted `rate_limited_until`, no
+ session-affinity clear), `persistCodexQuotaState` (no quota-state write,
+ no cache invalidation), `recordKeyHealthStatus` (key-health rotator
+ untouched)
+- OAuth refresh — both the proactive refresh in the executor base
+ (`base.ts` `execute()`, no refresh-token rotation consumed) and the
+ reactive 401/403 path in `chatCore` (no `expired` deactivation)
+- `chat.ts` — provider circuit breaker and the 5-minute quota cache
+ (`markAccountExhaustedFrom429`) never degraded
+
+The recorded error is what makes a flagged account visible in the dashboard
+while it stays serving traffic. Note: the probe record stores the **raw**
+(unsliced) error text, unlike the real path's `slice(0,100)` truncation.
+
+Operators who use test-all as a maintenance tool can restore the historical
+behavior (probe counts as a real generation) via either:
+
+- the `probeCanDisable` setting (`POST /api/settings` with
+ `{"probeCanDisable": true}`, or a direct `key_value` DB edit), or
+- feature flag **`PROBE_CAN_DISABLE=true`** (env or DB override; wins over the
+ setting).
+
+Fail-safe: if the flag or settings lookup throws, isolation stays ON.
+
## Source files
-| Concern | File |
-| --- | --- |
-| Signal tables + match | `open-sse/services/accountFallback.ts` |
-| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
-| Auto-disable scope | `src/shared/utils/autoDisableBanned.ts`, `src/sse/services/autoDisableBannedAccount.ts` |
-| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
-| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
-| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
-| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
+| Concern | File |
+| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
+| Signal tables + match | `open-sse/services/accountFallback.ts` |
+| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
+| Auto-disable scope | `src/shared/utils/autoDisableBanned.ts`, `src/sse/services/autoDisableBannedAccount.ts` |
+| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
+| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
+| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
+| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
diff --git a/docs/security/CLI_TOKEN.md b/docs/security/CLI_TOKEN.md
index 1e00e223346a..4d3383229db3 100644
--- a/docs/security/CLI_TOKEN.md
+++ b/docs/security/CLI_TOKEN.md
@@ -20,21 +20,26 @@ password on every invocation.
(falls back to an empty string on failure, disabling CLI auth).
2. It computes `HMAC-SHA256(machine_id, salt)` and returns the full 64-char
hex digest — a deterministic, non-reversible token tied to this machine.
-3. The CLI sends the token as `x-omniroute-cli-token` on every request to
- `http://localhost:/api/...`.
+3. The CLI sends the token as `x-omniroute-cli-token` only when the resolved
+ destination is an explicit loopback URL (`localhost`, `127.0.0.0/8`, or
+ loopback IPv6). Requests carrying the token use `redirect: error`, so a local
+ redirect cannot forward it to another origin. Remote contexts use scoped
+ access tokens instead. If derivation is unavailable, the CLI omits the header
+ and `omniroute doctor` reports the failure instead of treating an empty token
+ as valid.
4. The server (`src/server/authz/policies/management.ts`) recomputes the
expected token with the same salt and compares via `timingSafeEqual` to
prevent timing-based extraction.
## Security properties
-| Property | Detail |
-| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
-| **Loopback-only** | Accepted only when `Host` is `localhost`, `127.0.0.1`, or `::1`. |
-| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. |
-| **Non-reversible** | HMAC output cannot recover the machine-id. |
-| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. |
-| **Non-exportable** | Token is never written to disk or logged. |
+| Property | Detail |
+| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Loopback-only** | Accepted only when the server's trusted peer-locality stamp (derived from the real TCP peer address) says loopback. The client-controlled `Host` header is never trusted for locality. |
+| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. |
+| **Non-reversible** | HMAC output cannot recover the machine-id. |
+| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. |
+| **Non-exportable** | Token is never written to disk or logged. |
## Salt rotation
diff --git a/docs/security/COMPLIANCE.md b/docs/security/COMPLIANCE.md
index 738c6277baa0..30ef65a15d55 100644
--- a/docs/security/COMPLIANCE.md
+++ b/docs/security/COMPLIANCE.md
@@ -114,7 +114,7 @@ Two separate retention windows are honoured:
| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Row-cap trim for `proxy_logs` |
`cleanupExpiredLogs()` runs the retention pass. It is invoked on server startup
-from `src/server-init.ts` and `src/instrumentation-node.ts`. Each run logs a
+from `src/instrumentation-node.ts`. Each run logs a
`compliance.cleanup` audit event with the per-table delete counts. Proxy/call
log trimming is batched (`BATCH_SIZE = 5000`) to avoid long write locks.
diff --git a/electron/lib/remoteServerPreferences.js b/electron/lib/remoteServerPreferences.js
index 21b683290f1d..71425e37b94d 100644
--- a/electron/lib/remoteServerPreferences.js
+++ b/electron/lib/remoteServerPreferences.js
@@ -5,8 +5,8 @@ const path = require("path");
/**
* remoteServerPreferences.js — pure read/write helpers for the small JSON
- * preferences file that persists the operator-configured remote server URL
- * across app restarts (see resolveRemoteServerUrl.js for how it's consumed).
+ * preferences file that persists desktop-shell choices needed before the
+ * server-owned settings database is available.
*
* Deliberately a plain flat JSON file rather than the app's SQLite database:
* this preference must be readable before deciding whether to spawn (or even
@@ -18,19 +18,20 @@ const path = require("path");
* @param {string} prefsPath - absolute path to electron-preferences.json
* @param {(p: string) => boolean} [existsSync]
* @param {(p: string, enc: string) => string} [readFileSync]
- * @returns {{remoteServerUrl: string|null}}
+ * @returns {{remoteServerUrl: string|null, closeBehavior: "keep-loaded"|"unload"}}
*/
function readPreferences(prefsPath, existsSync = fs.existsSync, readFileSync = fs.readFileSync) {
- if (!existsSync(prefsPath)) return { remoteServerUrl: null };
+ if (!existsSync(prefsPath)) return { remoteServerUrl: null, closeBehavior: "keep-loaded" };
try {
const parsed = JSON.parse(readFileSync(prefsPath, "utf8"));
const remoteServerUrl =
typeof parsed.remoteServerUrl === "string" && parsed.remoteServerUrl.trim()
? parsed.remoteServerUrl.trim()
: null;
- return { remoteServerUrl };
+ const closeBehavior = parsed.closeBehavior === "unload" ? "unload" : "keep-loaded";
+ return { remoteServerUrl, closeBehavior };
} catch {
- return { remoteServerUrl: null };
+ return { remoteServerUrl: null, closeBehavior: "keep-loaded" };
}
}
@@ -72,4 +73,35 @@ function writeRemoteServerUrl(
}
}
-module.exports = { readPreferences, writeRemoteServerUrl };
+/** Persist whether closing the dashboard hides it or unloads its renderer. */
+function writeCloseBehavior(
+ prefsPath,
+ closeBehavior,
+ {
+ existsSync = fs.existsSync,
+ readFileSync = fs.readFileSync,
+ writeFileSync = fs.writeFileSync,
+ mkdirSync = fs.mkdirSync,
+ } = {}
+) {
+ try {
+ const dir = path.dirname(prefsPath);
+ if (!existsSync(dir)) {
+ mkdirSync(dir, { recursive: true });
+ }
+
+ const current = readPreferences(prefsPath, existsSync, readFileSync);
+ const next = {
+ ...current,
+ closeBehavior: closeBehavior === "unload" ? "unload" : "keep-loaded",
+ };
+ writeFileSync(prefsPath, JSON.stringify(next, null, 2) + "\n", "utf8");
+ } catch (err) {
+ console.error(
+ `[remoteServerPreferences] Failed to write preferences to ${prefsPath}:`,
+ err instanceof Error ? err.message : String(err)
+ );
+ }
+}
+
+module.exports = { readPreferences, writeRemoteServerUrl, writeCloseBehavior };
diff --git a/electron/lib/windowClosePolicy.js b/electron/lib/windowClosePolicy.js
new file mode 100644
index 000000000000..989e380d41d8
--- /dev/null
+++ b/electron/lib/windowClosePolicy.js
@@ -0,0 +1,26 @@
+"use strict";
+
+const CLOSE_BEHAVIOR_KEEP_LOADED = "keep-loaded";
+const CLOSE_BEHAVIOR_UNLOAD = "unload";
+
+function normalizeCloseBehavior(value) {
+ if (value === CLOSE_BEHAVIOR_KEEP_LOADED || value === CLOSE_BEHAVIOR_UNLOAD) return value;
+ return null;
+}
+
+function resolveRendererUrl(currentUrl, serverUrl) {
+ try {
+ const current = new URL(currentUrl);
+ const server = new URL(serverUrl);
+ return current.origin === server.origin ? current.href : server.href;
+ } catch {
+ return serverUrl;
+ }
+}
+
+module.exports = {
+ CLOSE_BEHAVIOR_KEEP_LOADED,
+ CLOSE_BEHAVIOR_UNLOAD,
+ normalizeCloseBehavior,
+ resolveRendererUrl,
+};
diff --git a/electron/lib/windowLifecycle.js b/electron/lib/windowLifecycle.js
new file mode 100644
index 000000000000..8a75a4a18dfe
--- /dev/null
+++ b/electron/lib/windowLifecycle.js
@@ -0,0 +1,28 @@
+/** Pure helpers for deciding and driving the Electron dashboard window lifecycle. */
+
+function shouldStartHidden({ argv = [], loginItemSettings = {} } = {}) {
+ return (
+ argv.includes("--hidden") ||
+ argv.includes("--minimized") ||
+ loginItemSettings.wasOpenedAsHidden === true
+ );
+}
+
+function showOrCreateWindow({ appReady, getWindow, createWindow }) {
+ if (!appReady) return null;
+
+ const currentWindow = getWindow();
+ if (!currentWindow || currentWindow.isDestroyed()) {
+ return createWindow();
+ }
+
+ if (currentWindow.isMinimized()) currentWindow.restore();
+ currentWindow.show();
+ currentWindow.focus();
+ return currentWindow;
+}
+
+module.exports = {
+ shouldStartHidden,
+ showOrCreateWindow,
+};
diff --git a/electron/main.js b/electron/main.js
index f052188ff3a0..19f232226b4a 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -38,8 +38,19 @@ const { killProcessTree } = require("./processTree");
const { resolveServerEntry } = require("./lib/resolveServerEntry");
const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper");
const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl");
-const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences");
+const {
+ readPreferences,
+ writeRemoteServerUrl,
+ writeCloseBehavior,
+} = require("./lib/remoteServerPreferences");
const { buildReadinessUrl, waitForServer } = require("./lib/serverReadiness");
+const { shouldStartHidden, showOrCreateWindow } = require("./lib/windowLifecycle");
+const {
+ CLOSE_BEHAVIOR_KEEP_LOADED,
+ CLOSE_BEHAVIOR_UNLOAD,
+ normalizeCloseBehavior,
+ resolveRendererUrl,
+} = require("./lib/windowClosePolicy");
// ── Single Instance Lock ───────────────────────────────────
const gotTheLock = app.requestSingleInstanceLock();
@@ -49,11 +60,12 @@ if (!gotTheLock) {
}
app.on("second-instance", () => {
- if (mainWindow) {
- if (mainWindow.isMinimized()) mainWindow.restore();
- mainWindow.show();
- mainWindow.focus();
- }
+ const isHeadless =
+ process.argv.includes("--headless") ||
+ process.argv.includes("--cli") ||
+ process.env.OMNIROUTE_HEADLESS === "true";
+ if (isHeadless) return;
+ showMainWindow();
});
// ── Environment Detection ──────────────────────────────────
@@ -71,6 +83,8 @@ let nextServer = null;
let serverPort = 20128;
let isServerStopped = false;
let remoteServerPromptWindow = null;
+let keepAliveWithoutWindows = false;
+let lastRendererUrl = null;
// ── Remote Server Mode ──────────────────────────────────────
// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
@@ -81,6 +95,8 @@ const REMOTE_SERVER_PREFS_PATH = path.join(
resolveDataDir(null, process.env),
"electron-preferences.json"
);
+const electronPreferences = readPreferences(REMOTE_SERVER_PREFS_PATH);
+let closeBehavior = electronPreferences.closeBehavior;
let remoteServerUrl = resolveRemoteServerUrl({
env: process.env,
prefsPath: REMOTE_SERVER_PREFS_PATH,
@@ -365,14 +381,18 @@ function setupContentSecurityPolicy() {
}
// ── Create Window ──────────────────────────────────────────
-function createWindow() {
+function createWindow({ showWhenReady = true } = {}) {
+ if (mainWindow && !mainWindow.isDestroyed()) return mainWindow;
+
+ const rendererStartedAt = Date.now();
+
// Platform-conditional options (#9)
const platformWindowOptions =
process.platform === "darwin"
? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 16 } }
: { titleBarStyle: "default" };
- mainWindow = new BrowserWindow({
+ const window = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
@@ -390,28 +410,28 @@ function createWindow() {
backgroundColor: "#0a0a0a",
...platformWindowOptions,
});
+ mainWindow = window;
// Load the Next.js app
- mainWindow.loadURL(getServerUrl());
+ window.loadURL(resolveRendererUrl(lastRendererUrl, getServerUrl()));
if (isDev) {
- mainWindow.webContents.openDevTools({ mode: "detach" });
+ window.webContents.openDevTools({ mode: "detach" });
}
- // Show window when ready (unless starting minimized/hidden in tray)
- mainWindow.once("ready-to-show", () => {
- const startHidden =
- process.argv.includes("--hidden") ||
- process.argv.includes("--minimized") ||
- app.getLoginItemSettings().wasOpenedAsHidden;
- if (!startHidden) {
- mainWindow.show();
+ // Hidden startup (createWindow({ showWhenReady: false })) skips the initial
+ // show(); the window stays created (so tray/dock interactions work) but the
+ // renderer only becomes visible on the next explicit showMainWindow() call.
+ window.once("ready-to-show", () => {
+ console.log(`[Electron] Renderer ready in ${Date.now() - rendererStartedAt}ms`);
+ if (showWhenReady) {
+ window.show();
} else {
console.log("[Electron] Launched hidden in background tray");
}
});
// Handle external links — validate URL protocol to prevent RCE
- mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ window.webContents.setWindowOpenHandler(({ url }) => {
try {
const parsedUrl = new URL(url);
if (["http:", "https:"].includes(parsedUrl.protocol)) {
@@ -425,20 +445,46 @@ function createWindow() {
return { action: "deny" };
});
- // Handle window close — minimize to tray
- mainWindow.on("close", (event) => {
+ // Keep the server alive while either hiding the renderer for a fast reopen or
+ // unloading it to reclaim memory, according to the persisted tray preference.
+ window.on("close", (event) => {
if (!app.isQuitting) {
event.preventDefault();
- mainWindow.hide();
+ lastRendererUrl = resolveRendererUrl(window.webContents.getURL(), getServerUrl());
+ if (closeBehavior === CLOSE_BEHAVIOR_UNLOAD) {
+ console.log("[Electron] Dashboard renderer unloaded; server remains running");
+ window.destroy();
+ } else {
+ console.log("[Electron] Dashboard hidden; renderer kept loaded");
+ window.hide();
+ }
}
return false;
});
- mainWindow.on("closed", () => {
- mainWindow = null;
+ window.on("closed", () => {
+ if (mainWindow === window) mainWindow = null;
+ });
+
+ return window;
+}
+
+function showMainWindow() {
+ return showOrCreateWindow({
+ appReady: app.isReady(),
+ getWindow: () => mainWindow,
+ createWindow,
});
}
+function setCloseBehavior(nextBehavior) {
+ const normalized = normalizeCloseBehavior(nextBehavior);
+ if (!normalized || normalized === closeBehavior) return;
+ closeBehavior = normalized;
+ writeCloseBehavior(REMOTE_SERVER_PREFS_PATH, closeBehavior);
+ createTray();
+}
+
// ── System Tray ────────────────────────────────────────────
function createTray() {
// Fix #4: Destroy old tray before recreating
@@ -465,12 +511,7 @@ function createTray() {
const contextMenu = Menu.buildFromTemplate([
{
label: "Open OmniRoute",
- click: () => {
- if (mainWindow) {
- mainWindow.show();
- mainWindow.focus();
- }
- },
+ click: () => showMainWindow(),
},
{
label: "Open Dashboard",
@@ -504,6 +545,23 @@ function createTray() {
},
],
},
+ {
+ label: "When Dashboard Closes",
+ submenu: [
+ {
+ label: "Keep Loaded (Faster Reopen)",
+ type: "radio",
+ checked: closeBehavior === CLOSE_BEHAVIOR_KEEP_LOADED,
+ click: () => setCloseBehavior(CLOSE_BEHAVIOR_KEEP_LOADED),
+ },
+ {
+ label: "Unload Renderer (Lower Memory)",
+ type: "radio",
+ checked: closeBehavior === CLOSE_BEHAVIOR_UNLOAD,
+ click: () => setCloseBehavior(CLOSE_BEHAVIOR_UNLOAD),
+ },
+ ],
+ },
{ type: "separator" },
{
label: "Check for Updates",
@@ -522,12 +580,7 @@ function createTray() {
tray.setToolTip("OmniRoute");
tray.setContextMenu(contextMenu);
- tray.on("double-click", () => {
- if (mainWindow) {
- mainWindow.show();
- mainWindow.focus();
- }
- });
+ tray.on("double-click", () => showMainWindow());
}
// ── Change Port (#3: now restarts server) ──────────────────
@@ -549,6 +602,7 @@ async function changePort(newPort) {
await waitForServer(getServerReadinessUrl());
// Reload window and update tray
+ lastRendererUrl = getServerUrl();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.loadURL(getServerUrl());
}
@@ -613,6 +667,7 @@ async function setRemoteServerUrl(nextUrl) {
remoteServerUrl = normalized;
writeRemoteServerUrl(REMOTE_SERVER_PREFS_PATH, remoteServerUrl);
+ lastRendererUrl = getServerUrl();
startNextServer();
try {
@@ -781,6 +836,14 @@ function startNextServer() {
...serverEnv,
DATA_DIR: dataDir,
PORT: String(serverPort),
+ // Pin the embedded server to loopback. Next.js standalone binds to
+ // `process.env.HOSTNAME || '0.0.0.0'`, and Windows always exports
+ // HOSTNAME as the machine name — which resolves to the LAN address, so
+ // the server listens only there and 127.0.0.1 stays closed. The renderer
+ // then fails to load `http://localhost:`, "ready-to-show" never
+ // fires, and the window (created with `show: false`) is never shown.
+ // Mirrors scripts/dev/run-next-playwright.mjs, which already pins this.
+ HOSTNAME: "127.0.0.1",
NODE_ENV: "production",
ELECTRON_RUN_AS_NODE: "1",
NODE_PATH: resolveServerNodePath(serverEnv, resolvePackNodePaths(dataDir)),
@@ -1086,9 +1149,20 @@ app.whenReady().then(async () => {
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
+ const startHidden =
+ !isHeadless &&
+ shouldStartHidden({
+ argv: process.argv,
+ loginItemSettings: app.getLoginItemSettings(),
+ });
+ keepAliveWithoutWindows = startHidden;
// Fix #1: Start server and WAIT for readiness before showing window
startNextServer();
+ if (!isHeadless) {
+ createTray();
+ }
+
let serverReady = true;
if (!isDev) {
// Probe the lightweight auth-exempt endpoint instead of aggregating full monitoring state.
@@ -1097,9 +1171,10 @@ app.whenReady().then(async () => {
if (isHeadless) {
console.log("[Electron] Headless mode active — UI window and tray icon skipped");
+ } else if (startHidden) {
+ console.log("[Electron] Launched hidden in background tray without a renderer");
} else {
- createWindow();
- createTray();
+ showMainWindow();
}
setupIpcHandlers();
@@ -1107,7 +1182,7 @@ app.whenReady().then(async () => {
// If readiness timed out (e.g. very long first-launch migrations), don't leave the
// window stuck on a hanging connection — keep polling and reload once it responds (#2460).
- if (!isDev && !serverReady && !isHeadless) {
+ if (!isDev && !serverReady && !isHeadless && !startHidden) {
void waitForServer(getServerReadinessUrl(), 300000).then((ready) => {
if (ready && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.loadURL(getServerUrl());
@@ -1125,11 +1200,7 @@ app.whenReady().then(async () => {
// macOS: recreate window when dock icon clicked
app.on("activate", () => {
if (isHeadless) return;
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- } else if (mainWindow) {
- mainWindow.show();
- }
+ showMainWindow();
});
});
@@ -1139,7 +1210,12 @@ app.on("window-all-closed", () => {
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
- if (process.platform !== "darwin" && !isHeadless) {
+ if (
+ process.platform !== "darwin" &&
+ !isHeadless &&
+ !keepAliveWithoutWindows &&
+ closeBehavior !== CLOSE_BEHAVIOR_UNLOAD
+ ) {
app.quit();
}
});
diff --git a/electron/package.json b/electron/package.json
index a3793de8fa40..57789a431891 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -64,9 +64,11 @@
"remoteServerPromptRenderer.js",
"lib/resolveServerEntry.js",
"lib/resolveNodeHelper.js",
+ "lib/windowLifecycle.js",
"lib/resolveRemoteServerUrl.js",
"lib/remoteServerPreferences.js",
"lib/serverReadiness.js",
+ "lib/windowClosePolicy.js",
"assets/remoteServerPrompt.html",
"package.json",
"node_modules/**/*"
diff --git a/examples/quickstart/python_requests.py b/examples/quickstart/python_requests.py
index ab838d8eb7c8..a27c7b38b00b 100644
--- a/examples/quickstart/python_requests.py
+++ b/examples/quickstart/python_requests.py
@@ -26,3 +26,8 @@
response = requests.post(API_URL, headers=headers, json=data)
response.raise_for_status()
print(response.json()["choices"][0]["message"]["content"])
+
+# Fresh install, zero credentials — `auto` already works:
+# curl http://localhost:20128/v1/chat/completions \
+# -H "Content-Type: application/json" \
+# -d '{"model":"auto","messages":[{"role":"user","content":"Hello!"}]}'
diff --git a/llm.txt b/llm.txt
index 92016efd1c84..da0da5334eb7 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 341 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in 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
@@ -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
@@ -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 (341), 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
-- **341 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
@@ -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.
@@ -475,7 +475,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
-- **341-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/next.config.mjs b/next.config.mjs
index 2f22b8aebd0c..df3f6e32c4bf 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/agyModels.ts b/open-sse/config/agyModels.ts
index 412e073044bc..5e9f37b84eaf 100644
--- a/open-sse/config/agyModels.ts
+++ b/open-sse/config/agyModels.ts
@@ -41,6 +41,15 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
+ {
+ id: "gemini-3.7-flash-tiered",
+ name: "Gemini 3.7 Flash (Tiered)",
+ contextLength: 1048576,
+ maxOutputTokens: 65536,
+ supportsReasoning: true,
+ supportsVision: true,
+ toolCalling: true,
+ },
// Gemini 3.1 Pro
{
id: "gemini-pro-agent",
diff --git a/open-sse/config/antigravityModelAliases.ts b/open-sse/config/antigravityModelAliases.ts
index 5eab3f4c8e83..3946b776d08d 100644
--- a/open-sse/config/antigravityModelAliases.ts
+++ b/open-sse/config/antigravityModelAliases.ts
@@ -29,6 +29,15 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
+ {
+ id: "gemini-3.7-flash-tiered",
+ name: "Gemini 3.7 Flash (Tiered)",
+ contextLength: 1048576,
+ maxOutputTokens: 65536,
+ supportsReasoning: true,
+ supportsVision: true,
+ toolCalling: true,
+ },
// Gemini 3.1 Pro budget tiers. Live streamGenerateContent validation uses
// `gemini-pro-agent` for High; the separately advertised `gemini-3.1-pro-high`
// discovery slot currently returns HTTP 400 and is intentionally not public.
@@ -91,6 +100,13 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
]);
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
+ // Gemini 3.7 Flash tiers map to the upstream tiered endpoint model; the thinking
+ // budget is steered via generationConfig.thinkingConfig.thinkingBudget.
+ "gemini-3.7-flash": "gemini-3.7-flash-tiered",
+ "gemini-3.7-flash-high": "gemini-3.7-flash-tiered",
+ "gemini-3.7-flash-medium": "gemini-3.7-flash-tiered",
+ "gemini-3.7-flash-low": "gemini-3.7-flash-tiered",
+ "gpt-oss-120b": "gpt-oss-120b-medium",
// gemini-3.1-pro-low is not aliased: the upstream accepts it verbatim.
// gemini-3.1-pro-high: the discovery slot returns HTTP 400 on v1internal;
// the live upstream id is gemini-pro-agent (see ANTIGRAVITY_PUBLIC_MODELS).
diff --git a/open-sse/config/codexClient.ts b/open-sse/config/codexClient.ts
index 0cb1b05772ae..5c71e931a529 100644
--- a/open-sse/config/codexClient.ts
+++ b/open-sse/config/codexClient.ts
@@ -1,9 +1,13 @@
import {
+ CODEX_CLI_RS_ORIGINATOR,
DEFAULT_CODEX_CLIENT_VERSION,
getCodexCliRsHeaders as buildCodexCliRsHeaders,
} from "@/shared/constants/codexClient";
-export { DEFAULT_CODEX_CLIENT_VERSION } from "@/shared/constants/codexClient";
+export {
+ DEFAULT_CODEX_CLIENT_VERSION,
+ CODEX_CLI_RS_ORIGINATOR,
+} from "@/shared/constants/codexClient";
const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26200";
const DEFAULT_CODEX_USER_AGENT_ARCH = "x64";
const CODEX_VERSION_OVERRIDE_ENV = "CODEX_CLIENT_VERSION";
@@ -51,6 +55,35 @@ export function getCodexCliRsHeaders(): Record {
return buildCodexCliRsHeaders(getCodexClientVersion());
}
+/**
+ * Identity for the credential face (auth.openai.com: token exchange / refresh).
+ * The real Codex client sends only `originator` + `User-Agent` on that face
+ * (codex-rs login/default_client.rs default_headers()); the `Version` header
+ * gate exists only on the chatgpt.com/backend-api inference face, so it is
+ * deliberately omitted here. Mirrors sub2api v0.1.178
+ * ApplyCodexCanonicalAuthIdentity.
+ */
+export function getCodexAuthIdentityHeaders(): Record {
+ return {
+ "User-Agent": getCodexUserAgent(),
+ originator: CODEX_CLI_RS_ORIGINATOR,
+ };
+}
+
+/**
+ * Canonical Codex CLI identity for server-initiated calls against the
+ * chatgpt.com/backend-api face that are not tied to one end-client request
+ * (usage / quota / models manifest / reset-credits). Same UA/version chain as
+ * inference so these calls do not show up upstream as anonymous half-identities.
+ */
+export function getCodexBackendIdentityHeaders(): Record {
+ return {
+ "User-Agent": getCodexUserAgent(),
+ originator: CODEX_CLI_RS_ORIGINATOR,
+ Version: getCodexClientVersion(),
+ };
+}
+
export function normalizeCodexSessionId(value: unknown): string | null {
if (typeof value !== "string") return null;
const normalized = value.trim();
diff --git a/open-sse/config/codexIdentity.ts b/open-sse/config/codexIdentity.ts
index bc45fa1cf6e3..a081c5402b0c 100644
--- a/open-sse/config/codexIdentity.ts
+++ b/open-sse/config/codexIdentity.ts
@@ -1,15 +1,30 @@
import { createHash, randomUUID } from "node:crypto";
import { normalizeCodexSessionId } from "./codexClient.ts";
+import { isCrossAccountCodexTurnState, readCodexTurnStateHeader } from "./codexTurnState.ts";
const CODEX_INSTALLATION_SALT = "omniroute-codex-installation";
const CODEX_SESSION_SEED_PREFIX = "omniroute:codex-session-id:v1:";
const CODEX_THREAD_SEED_PREFIX = "omniroute:codex-thread-id:v1:";
+// v2 derivations are keyed by the persisted per-connection random seed
+// (codexFingerprintSeed) instead of the connection-id chain, mirroring
+// sub2api v0.1.178 (#5696): deterministic derivation stays stable, but the
+// seed is generated per connection so identities never collide across
+// deployments and survive connection export/import.
+const CODEX_INSTALLATION_SEED_PREFIX_V2 = "omniroute:codex-installation:v2:";
+const CODEX_SESSION_SEED_PREFIX_V2 = "omniroute:codex-session-id:v2:";
+const CODEX_THREAD_SEED_PREFIX_V2 = "omniroute:codex-thread-id:v2:";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export const CODEX_FINGERPRINT_MODES = ["off", "device", "session", "full"] as const;
export type CodexFingerprintMode = (typeof CODEX_FINGERPRINT_MODES)[number];
export const CODEX_FINGERPRINT_MODE_KEY = "codexFingerprintMode";
+/**
+ * System-managed per-connection random seed used as the fingerprint
+ * derivation source. Never sent upstream, stripped from API responses, and
+ * preserved across connection updates (sub2api `codex_fingerprint_seed`).
+ */
+export const CODEX_FINGERPRINT_SEED_KEY = "codexFingerprintSeed";
export type CodexClientIdentity = {
mode: CodexFingerprintMode;
@@ -72,6 +87,61 @@ function accountSeed(
);
}
+/** The persisted system-managed random seed, when present and a valid UUID. */
+export function getCodexFingerprintSeed(
+ providerSpecificData?: Record | null
+): string | null {
+ return normalizeUuid(providerSpecificData?.[CODEX_FINGERPRINT_SEED_KEY]);
+}
+
+/** Modes that rewrite account-scoped identifiers and therefore need a stable seed. */
+export function codexFingerprintModeRequiresSeed(mode: CodexFingerprintMode): boolean {
+ return mode === "device" || mode === "session" || mode === "full";
+}
+
+/**
+ * Ensure a Codex OAuth connection carries a persisted fingerprint seed when its
+ * convergence mode derives account-scoped identifiers. Called at connection
+ * create/update time (the persistence layer owns the write); the request path
+ * only ever READS the seed, so an identity never rotates mid-flight.
+ *
+ * Semantics mirror sub2api v0.1.178 `prepareCodexFingerprintExtraFor{Create,Update}`:
+ * - the key is system-managed: any client-supplied value is stripped first;
+ * - an existing valid seed is ALWAYS carried forward (even when the new mode
+ * is `off` — it stays dormant, ready if convergence is re-enabled later);
+ * - otherwise a fresh seed is created only when the mode requires one
+ * (device/session/full; the OmniRoute default is session).
+ *
+ * Returns the (possibly new) providerSpecificData, or undefined when there is
+ * nothing to store. Pre-seed connections keep their legacy connection-id
+ * derived identity until the next save — one deliberate rotation, same as
+ * sub2api's migration-225 backfill.
+ */
+export function ensureCodexFingerprintSeed(
+ providerSpecificData?: Record | null,
+ credentials?: { accessToken?: unknown; refreshToken?: unknown } | null,
+ existingProviderSpecificData?: Record | null
+): Record | undefined {
+ const psd: Record = { ...(providerSpecificData || {}) };
+ // System-managed key: never trust an inbound value, regardless of auth type.
+ delete psd[CODEX_FINGERPRINT_SEED_KEY];
+ if (!isCodexOAuthCredentials(credentials)) {
+ return Object.keys(psd).length > 0 ? psd : undefined;
+ }
+
+ const existingSeed = getCodexFingerprintSeed(existingProviderSpecificData);
+ if (existingSeed) {
+ psd[CODEX_FINGERPRINT_SEED_KEY] = existingSeed;
+ return psd;
+ }
+ const mode = getCodexFingerprintMode(psd, true);
+ if (codexFingerprintModeRequiresSeed(mode)) {
+ psd[CODEX_FINGERPRINT_SEED_KEY] = randomUUID();
+ return psd;
+ }
+ return Object.keys(psd).length > 0 ? psd : undefined;
+}
+
function readNamedHeader(
headers: Headers | Record | null | undefined,
name: string
@@ -120,6 +190,11 @@ export function getCodexInstallationId(
const explicit = normalizeUuid(providerSpecificData?.codexInstallationId);
if (explicit) return explicit;
+ const persistedSeed = getCodexFingerprintSeed(providerSpecificData);
+ if (persistedSeed) {
+ return deriveStableUUIDv4(`${CODEX_INSTALLATION_SEED_PREFIX_V2}${persistedSeed}`);
+ }
+
const legacyStableSource =
nonEmptyString(providerSpecificData?.workspaceId) ||
nonEmptyString(providerSpecificData?.accountId) ||
@@ -137,6 +212,10 @@ export function getCodexConvergedSessionId(
providerSpecificData?: Record | null,
accountKey?: string | null
): string {
+ const persistedSeed = getCodexFingerprintSeed(providerSpecificData);
+ if (persistedSeed) {
+ return deriveStableUUIDv4(`${CODEX_SESSION_SEED_PREFIX_V2}${persistedSeed}`);
+ }
return deriveStableUUIDv4(
`${CODEX_SESSION_SEED_PREFIX}${accountSeed(providerSpecificData, accountKey)}`
);
@@ -148,6 +227,10 @@ export function getCodexConvergedThreadId(
accountKey?: string | null
): string {
if (!nonEmptyString(clientSessionId)) return "";
+ const persistedSeed = getCodexFingerprintSeed(providerSpecificData);
+ if (persistedSeed) {
+ return deriveStableUUIDv4(`${CODEX_THREAD_SEED_PREFIX_V2}${persistedSeed}:${clientSessionId}`);
+ }
return deriveStableUUIDv4(
`${CODEX_THREAD_SEED_PREFIX}${accountSeed(providerSpecificData, accountKey)}:${clientSessionId}`
);
@@ -163,6 +246,26 @@ export function getCodexClientSessionId(
);
}
+/**
+ * Decide what to do with the client's `x-codex-turn-state` echo for the
+ * account about to serve this request. The blob is minted per account by the
+ * upstream; replaying another account's blob after failover is a proxy-only
+ * contradiction, so a known cross-account echo is stripped. Same-account or
+ * unknown provenance passes through unchanged (strip only, never inject).
+ * Independent of the fingerprint-convergence mode — account consistency also
+ * applies to explicit `off` / passthrough.
+ */
+export function resolveCodexTurnStateEcho(
+ clientHeaders?: Headers | Record | null,
+ accountKey?: string | null
+): string | null {
+ const value = readCodexTurnStateHeader(clientHeaders);
+ if (!value) return null;
+ const sessionId = getCodexClientSessionId(clientHeaders);
+ if (sessionId && isCrossAccountCodexTurnState(sessionId, accountKey)) return null;
+ return value;
+}
+
/**
* One identity object for every carrier in one upstream turn.
* accountKey may be the OmniRoute connection id; it is never sent upstream.
@@ -284,13 +387,19 @@ export function withCodexFingerprintCredentials();
+let turnStateWrites = 0;
+
+function normalizeAccountKey(accountKey: unknown): string | null {
+ if (typeof accountKey !== "string") return null;
+ const trimmed = accountKey.trim();
+ return trimmed || null;
+}
+
+/**
+ * Read the turn-state blob from a headers bag (Headers instance or a plain
+ * record with arbitrary casing). Returns null when absent/blank.
+ */
+export function readCodexTurnStateHeader(
+ headers: Headers | Record | null | undefined
+): string | null {
+ if (!headers) return null;
+ if (headers instanceof Headers) {
+ const value = headers.get(CODEX_TURN_STATE_HEADER);
+ return typeof value === "string" && value.trim() ? value.trim() : null;
+ }
+ if (typeof headers === "object") {
+ for (const [key, value] of Object.entries(headers)) {
+ if (
+ key.toLowerCase() === CODEX_TURN_STATE_HEADER &&
+ typeof value === "string" &&
+ value.trim()
+ ) {
+ return value.trim();
+ }
+ }
+ }
+ return null;
+}
+
+function sweepExpiredTurnStateOrigins(now: number): void {
+ for (const [key, origin] of turnStateOrigins) {
+ if (origin.expiresAt <= now) {
+ turnStateOrigins.delete(key);
+ }
+ }
+}
+
+/**
+ * Record that `accountKey` minted the turn-state blob this downstream session
+ * just received. Must only be called at the response commit point — when the
+ * header is actually written to the client. Recording earlier (e.g. for an
+ * attempt later discarded by failover) would poison the table and make the
+ * guard strip the NEXT account's legitimate echo.
+ */
+export function noteCodexTurnStateProvenance(
+ clientSessionId: string | null | undefined,
+ accountKey: unknown,
+ nowMs?: number
+): void {
+ const sessionId = typeof clientSessionId === "string" ? clientSessionId.trim() : "";
+ const account = normalizeAccountKey(accountKey);
+ if (!sessionId || !account) return;
+
+ const now = typeof nowMs === "number" ? nowMs : Date.now();
+ turnStateOrigins.set(sessionId, {
+ accountKey: account,
+ expiresAt: now + CODEX_TURN_STATE_TTL_MS,
+ });
+
+ turnStateWrites += 1;
+ if (turnStateWrites % CODEX_TURN_STATE_SWEEP_EVERY_WRITES === 0) {
+ sweepExpiredTurnStateOrigins(now);
+ }
+}
+
+/**
+ * Outbound guard: true when the echoed blob is KNOWN to have been minted by a
+ * different account and must be stripped before going upstream. Same-account
+ * or unknown provenance passes through unchanged — stripping only, never
+ * injection (clients that cannot echo are the Claude bridge's concern, not
+ * this module's).
+ */
+export function isCrossAccountCodexTurnState(
+ clientSessionId: string | null | undefined,
+ accountKey: unknown,
+ nowMs?: number
+): boolean {
+ const sessionId = typeof clientSessionId === "string" ? clientSessionId.trim() : "";
+ const account = normalizeAccountKey(accountKey);
+ if (!sessionId || !account) return false;
+
+ const origin = turnStateOrigins.get(sessionId);
+ if (!origin) return false;
+ const now = typeof nowMs === "number" ? nowMs : Date.now();
+ if (origin.expiresAt <= now) {
+ turnStateOrigins.delete(sessionId);
+ return false;
+ }
+ return origin.accountKey !== account;
+}
+
+/** Test hook: forget all provenance records and reset the sweep counter. */
+export function __resetCodexTurnStateOriginsForTesting(): void {
+ turnStateOrigins.clear();
+ turnStateWrites = 0;
+}
diff --git a/open-sse/config/errorConfig.ts b/open-sse/config/errorConfig.ts
index b326af737872..7e7355948f19 100644
--- a/open-sse/config/errorConfig.ts
+++ b/open-sse/config/errorConfig.ts
@@ -81,6 +81,10 @@ export const COOLDOWN_MS = {
// account, so re-probe only after a long window (or when the operator routes
// egress through a supported-region proxy).
geoBlocked: 24 * 60 * 60 * 1000,
+ // Antigravity BYOP (GCP_PROJECT_REQUIRED): nothing changes on the account
+ // until the operator enters a Project ID, so keep the connection excluded
+ // from selection for a long window (mirrors the geo-blocked treatment).
+ gcpProjectRequired: 24 * 60 * 60 * 1000,
};
/**
diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts
index 3d1777ed653b..fe30a37325dd 100644
--- a/open-sse/config/freeModelCatalog.data.ts
+++ b/open-sse/config/freeModelCatalog.data.ts
@@ -16,9 +16,11 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
* rewrites file timestamps on every deploy, which would report a months-old
* catalog as "updated today". Bump this whenever the entries below change.
*/
-export const FREE_CATALOG_CURATED_AT = "2026-08-16";
+export const FREE_CATALOG_CURATED_AT = "2026-08-18";
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
+ { provider: "chatgpt-web", modelId: "gpt-5.6-luna-free", displayName: "GPT-5.6 Luna (Free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
+ { provider: "chatgpt-web", modelId: "gpt-5.6-luna-free-thinking", displayName: "GPT-5.6 Luna (Free, Think)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
diff --git a/open-sse/config/glmProvider.ts b/open-sse/config/glmProvider.ts
index f8acfd4031fb..9c1580e7aedd 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/grokBuild.ts b/open-sse/config/grokBuild.ts
index c6e7dca7bfb1..ab648afd1821 100644
--- a/open-sse/config/grokBuild.ts
+++ b/open-sse/config/grokBuild.ts
@@ -11,6 +11,7 @@ export const GROK_BUILD_TOKEN_URL = `${GROK_BUILD_OAUTH_ISSUER}/oauth2/token`;
export const GROK_BUILD_DEFAULT_CLIENT_VERSION = "0.2.106";
export const GROK_BUILD_DEFAULT_CONTEXT_WINDOW = 256_000;
export const GROK_BUILD_DEFAULT_REASONING_EFFORT = "high";
+export const GROK_BUILD_SUPPORTED_REASONING_EFFORTS = Object.freeze(["low", "medium", "high"]);
export const GROK_BUILD_CLIENT_IDENTIFIER = "grok-shell";
export const GROK_BUILD_TOKEN_AUTH = "xai-grok-cli";
export const GROK_BUILD_REASONING_INCLUDE = "reasoning.encrypted_content";
diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts
index d50865599db2..02019dc4a01a 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 {
@@ -210,6 +210,7 @@ export const IMAGE_PROVIDERS: Record = {
authHeader: "bearer",
format: "openai", // native OpenAI format
models: [
+ { id: "dall-e-3", name: "DALL·E 3" },
{ id: "gpt-image-2", name: "GPT Image 2" },
{ id: "gpt-image-1.5", name: "GPT Image 1.5" },
{ id: "gpt-image-1-mini", name: "GPT Image 1 Mini" },
@@ -267,6 +268,25 @@ export const IMAGE_PROVIDERS: Record = {
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
},
+ // Cursor plan image generation via the Agent CLI native `generateImage` tool.
+ // Reuses the same OAuth/API-key connection as chat (`provider: "cursor"`).
+ // Requires the `agent` binary (CURSOR_AGENT_BIN) — see cursorAgentImage handler.
+ cursor: {
+ id: "cursor",
+ alias: "cu",
+ // Sentinel: execution is local Agent CLI, not an HTTP image API.
+ baseUrl: "agent://cursor-agent",
+ authType: "oauth",
+ authHeader: "bearer",
+ format: "cursor-agent-image",
+ models: [
+ { id: "auto", name: "Cursor Auto (Image)" },
+ { id: "composer-2", name: "Composer 2 (Image)" },
+ { id: "composer-2.5", name: "Composer 2.5 (Image)" },
+ ],
+ supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "1024x1536", "1536x1024"],
+ },
+
"microsoft-designer-web": {
id: "microsoft-designer-web",
alias: "msdesigner",
@@ -475,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",
@@ -864,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;
}
/**
@@ -898,9 +923,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 };
}
}
@@ -1001,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/musicRegistry.ts b/open-sse/config/musicRegistry.ts
index 06aa8a159be6..fd1f88fd88f9 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/config/providers/alternateFormats.ts b/open-sse/config/providers/alternateFormats.ts
index b223a26448c7..8a7deb782c6c 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 01395e7dd87c..00861b24225d 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";
@@ -16,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";
@@ -65,7 +68,7 @@ import { api_airforceProvider } from "./registry/api-airforce/index.ts";
import { mistralProvider } from "./registry/mistral/index.ts";
import { togetherProvider } from "./registry/together/index.ts";
import { cohereProvider } from "./registry/cohere/index.ts";
-import { cursorProvider } from "./registry/cursor/index.ts";
+import { cursorProvider, cursor_apiProvider } from "./registry/cursor/index.ts";
import { volcengineProvider } from "./registry/volcengine/index.ts";
import { hackclubProvider } from "./registry/hackclub/index.ts";
import { freetheaiProvider } from "./registry/freetheai/index.ts";
@@ -213,6 +216,7 @@ import { kiroProvider } from "./registry/kiro/index.ts";
import { openadapterProvider } from "./registry/openadapter/index.ts";
import { ditProvider } from "./registry/dit/index.ts";
import { tokenrouterProvider } from "./registry/tokenrouter/index.ts";
+import { token_kioskProvider } from "./registry/token-kiosk/index.ts";
import { grok_cliProvider } from "./registry/grok-cli/index.ts";
import { codebuddy_cnProvider } from "./registry/codebuddy-cn/index.ts";
import { pioneerProvider } from "./registry/pioneer/index.ts";
@@ -260,9 +264,12 @@ 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,
+ "mlx-gemma": mlxGemmaProvider,
+ "mlx-qwen": mlxQwenProvider,
"ollama-cloud": ollama_cloudProvider,
synthetic: syntheticProvider,
ideogram: ideogramProvider,
@@ -276,6 +283,7 @@ export const REGISTRY: Record = {
deepai: deepaiProvider,
nebius: nebiusProvider,
fireworks: fireworksProvider,
+ freebuff: freebuffProvider,
llamagate: llamagateProvider,
glm: glmProvider,
glmt: glmtProvider,
@@ -326,6 +334,7 @@ export const REGISTRY: Record = {
together: togetherProvider,
cohere: cohereProvider,
cursor: cursorProvider,
+ "cursor-api": cursor_apiProvider,
volcengine: volcengineProvider,
hackclub: hackclubProvider,
freetheai: freetheaiProvider,
@@ -475,6 +484,7 @@ export const REGISTRY: Record = {
openadapter: openadapterProvider,
dit: ditProvider,
tokenrouter: tokenrouterProvider,
+ "token-kiosk": token_kioskProvider,
"grok-cli": grok_cliProvider,
"codebuddy-cn": codebuddy_cnProvider,
pioneer: pioneerProvider,
@@ -523,4 +533,5 @@ export const REGISTRY: Record = {
"free-ai": freeAiProvider,
"void-ai": voidAiProvider,
helixmind: helixmindProvider,
+ tabitoken: tabitokenProvider,
};
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 a1ccb6b13c8a..1c290668f900 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/chatgpt-web/index.ts b/open-sse/config/providers/registry/chatgpt-web/index.ts
index bb57c7f31c26..16c1522e2b68 100644
--- a/open-sse/config/providers/registry/chatgpt-web/index.ts
+++ b/open-sse/config/providers/registry/chatgpt-web/index.ts
@@ -9,12 +9,83 @@ export const chatgpt_webProvider: RegistryEntry = {
authType: "apikey",
authHeader: "cookie",
models: [
- { id: "gpt-5.6-pro", name: "GPT-5.6 Pro", toolCalling: false }, // pro tier only, standard effort
- { id: "gpt-5.6-thinking", name: "GPT-5.6 Thinking", toolCalling: false }, // plus, pro tier
- { id: "gpt-5.5-pro-extended", name: "GPT-5.5 Pro Extended", toolCalling: false }, // pro tier only, extended effort
- { id: "gpt-5.5-pro", name: "GPT-5.5 Pro", toolCalling: false }, // pro tier only, standard effort
- { id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking", toolCalling: false }, // plus, pro tier
- { id: "gpt-5.5", name: "GPT-5.5 Instant", toolCalling: false }, // free, plus, pro tier
- { id: "o3", name: "o3", toolCalling: false }, // plus ~ tier
+ {
+ id: "gpt-5.6-sol-pro",
+ name: "GPT-5.6 Sol (Pro)",
+ liveCatalogIds: ["gpt-5-6-pro"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.6-sol-xhigh",
+ name: "GPT-5.6 Sol (Xhigh)",
+ liveCatalogIds: ["gpt-5-6-thinking"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.6-sol-high",
+ name: "GPT-5.6 Sol (High)",
+ liveCatalogIds: ["gpt-5-6-thinking"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.6-sol-medium",
+ name: "GPT-5.6 Sol (Medium)",
+ liveCatalogIds: ["gpt-5-6-thinking"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.6-sol-instant",
+ name: "GPT-5.6 Sol (Instant)",
+ liveCatalogIds: ["gpt-5-6"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.6-luna-free-thinking",
+ name: "GPT-5.6 Luna (Free, Think)",
+ liveCatalogIds: ["gpt-5-6"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.6-luna-free",
+ name: "GPT-5.6 Luna (Free)",
+ liveCatalogIds: ["gpt-5-6"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.5-pro-extended",
+ name: "GPT-5.5 (Pro Extended)",
+ liveCatalogIds: ["gpt-5-5-pro"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.5-pro",
+ name: "GPT-5.5 (Pro)",
+ liveCatalogIds: ["gpt-5-5-pro"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.5-xhigh",
+ name: "GPT-5.5 (Xhigh)",
+ liveCatalogIds: ["gpt-5-5-thinking"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.5-high",
+ name: "GPT-5.5 (High)",
+ liveCatalogIds: ["gpt-5-5-thinking"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.5-medium",
+ name: "GPT-5.5 (Medium)",
+ liveCatalogIds: ["gpt-5-5-thinking"],
+ toolCalling: false,
+ },
+ {
+ id: "gpt-5.5-instant",
+ name: "GPT-5.5 (Instant)",
+ liveCatalogIds: ["gpt-5-5"],
+ toolCalling: false,
+ },
],
};
diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts
index 7d6fee4557e7..6b67797fa3d4 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/command-code/index.ts b/open-sse/config/providers/registry/command-code/index.ts
index 23a73efe462c..77b08ab8ce80 100644
--- a/open-sse/config/providers/registry/command-code/index.ts
+++ b/open-sse/config/providers/registry/command-code/index.ts
@@ -1,5 +1,7 @@
import type { RegistryEntry } from "../../shared.ts";
+const COMMAND_CODE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
+
export const command_codeProvider: RegistryEntry = {
id: "command-code",
alias: "cmd",
@@ -20,6 +22,7 @@ export const command_codeProvider: RegistryEntry = {
id: "claude-opus-4-7",
name: "Claude Opus 4.7 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 200000,
maxOutputTokens: 32000,
@@ -28,6 +31,7 @@ export const command_codeProvider: RegistryEntry = {
id: "claude-opus-4-6",
name: "Claude Opus 4.6 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 200000,
maxOutputTokens: 32000,
@@ -36,6 +40,7 @@ export const command_codeProvider: RegistryEntry = {
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 200000,
maxOutputTokens: 16384,
@@ -44,6 +49,7 @@ export const command_codeProvider: RegistryEntry = {
id: "claude-haiku-4-5-20251001",
name: "Claude Haiku 4.5 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 200000,
maxOutputTokens: 8192,
@@ -52,6 +58,7 @@ export const command_codeProvider: RegistryEntry = {
id: "gpt-5.5",
name: "GPT-5.5 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 256000,
maxOutputTokens: 128000,
@@ -60,6 +67,7 @@ export const command_codeProvider: RegistryEntry = {
id: "gpt-5.4",
name: "GPT-5.4 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 256000,
maxOutputTokens: 128000,
@@ -68,6 +76,7 @@ export const command_codeProvider: RegistryEntry = {
id: "gpt-5.3-codex",
name: "GPT-5.3 Codex (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 256000,
maxOutputTokens: 128000,
@@ -75,7 +84,8 @@ export const command_codeProvider: RegistryEntry = {
{
id: "gpt-5.4-mini",
name: "GPT-5.4 Mini (CC)",
- supportsReasoning: false,
+ supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 256000,
maxOutputTokens: 128000,
@@ -84,6 +94,7 @@ export const command_codeProvider: RegistryEntry = {
id: "deepseek/deepseek-v4-pro",
name: "DeepSeek V4 Pro (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 1000000,
maxOutputTokens: 131072,
},
@@ -91,6 +102,7 @@ export const command_codeProvider: RegistryEntry = {
id: "deepseek/deepseek-v4-flash",
name: "DeepSeek V4 Flash (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 1000000,
maxOutputTokens: 131072,
},
@@ -98,6 +110,7 @@ export const command_codeProvider: RegistryEntry = {
id: "moonshotai/Kimi-K2.6",
name: "Kimi K2.6 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 262144,
maxOutputTokens: 65536,
@@ -106,6 +119,7 @@ export const command_codeProvider: RegistryEntry = {
id: "moonshotai/Kimi-K2.5",
name: "Kimi K2.5 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 262144,
maxOutputTokens: 65536,
@@ -114,6 +128,7 @@ export const command_codeProvider: RegistryEntry = {
id: "zai-org/GLM-5.1",
name: "GLM-5.1 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 200000,
maxOutputTokens: 32768,
},
@@ -121,6 +136,7 @@ export const command_codeProvider: RegistryEntry = {
id: "zai-org/GLM-5",
name: "GLM-5 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 200000,
maxOutputTokens: 32768,
},
@@ -128,6 +144,7 @@ export const command_codeProvider: RegistryEntry = {
id: "MiniMaxAI/MiniMax-M2.7",
name: "MiniMax M2.7 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 1048576,
maxOutputTokens: 65536,
},
@@ -135,6 +152,7 @@ export const command_codeProvider: RegistryEntry = {
id: "MiniMaxAI/MiniMax-M2.5",
name: "MiniMax M2.5 (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 1048576,
maxOutputTokens: 65536,
},
@@ -142,6 +160,7 @@ export const command_codeProvider: RegistryEntry = {
id: "Qwen/Qwen3.6-Max-Preview",
name: "Qwen 3.6 Max (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
contextLength: 1000000,
maxOutputTokens: 32768,
},
@@ -149,6 +168,7 @@ export const command_codeProvider: RegistryEntry = {
id: "Qwen/Qwen3.6-Plus",
name: "Qwen 3.6 Plus (CC)",
supportsReasoning: true,
+ supportedThinkingEfforts: COMMAND_CODE_REASONING_EFFORTS,
supportsVision: true,
contextLength: 1000000,
maxOutputTokens: 32768,
diff --git a/open-sse/config/providers/registry/cursor/index.ts b/open-sse/config/providers/registry/cursor/index.ts
index 0e167d39bbda..1bb6d02b214e 100644
--- a/open-sse/config/providers/registry/cursor/index.ts
+++ b/open-sse/config/providers/registry/cursor/index.ts
@@ -156,3 +156,29 @@ export const cursorProvider: RegistryEntry = {
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
],
};
+
+/**
+ * API-key variant of the Cursor provider.
+ *
+ * Same wire protocol, executor and catalog as `cursor`, but the connection
+ * holds a Cursor user API key (`crsr_…`, cursor.com/dashboard/api) instead of
+ * an IDE/OAuth session. The executor exchanges that key for a session token
+ * on demand (open-sse/services/cursorApiKeyAuth.ts), so no cursor-agent or
+ * IDE install is needed on the OmniRoute host. Kept as a distinct backend ID
+ * so API-key and IDE-session connections never share renewal, quota or
+ * dashboard semantics.
+ */
+export const cursor_apiProvider: RegistryEntry = {
+ id: "cursor-api",
+ alias: "cua",
+ format: cursorProvider.format,
+ executor: "cursor-api",
+ baseUrl: cursorProvider.baseUrl,
+ chatPath: cursorProvider.chatPath,
+ authType: "apikey",
+ authHeader: "bearer",
+ defaultContextLength: cursorProvider.defaultContextLength,
+ headers: getCursorRegistryHeaders(),
+ clientVersion: CURSOR_REGISTRY_VERSION,
+ models: cursorProvider.models,
+};
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 000000000000..713f46954830
--- /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/config/providers/registry/freepik/index.ts b/open-sse/config/providers/registry/freepik/index.ts
deleted file mode 100644
index 7b99c711682e..000000000000
--- 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/gemini/index.ts b/open-sse/config/providers/registry/gemini/index.ts
index 8119ed9c41cf..468fcd888903 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/grok-cli/index.ts b/open-sse/config/providers/registry/grok-cli/index.ts
index f257f8d60a62..e65ced0e76b3 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/hcnsec/index.ts b/open-sse/config/providers/registry/hcnsec/index.ts
index acce62c2bc29..dba2b690dafb 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/magnific/index.ts b/open-sse/config/providers/registry/magnific/index.ts
new file mode 100644
index 000000000000..63c83d5b1ca3
--- /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/config/providers/registry/mlx/index.ts b/open-sse/config/providers/registry/mlx/index.ts
new file mode 100644
index 000000000000..24d3e8b23244
--- /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/config/providers/registry/muse-code/index.ts b/open-sse/config/providers/registry/muse-code/index.ts
index 66f59f9f4284..37db1e988d44 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/ollama-cloud/index.ts b/open-sse/config/providers/registry/ollama-cloud/index.ts
index 05b28df65f64..4cf020263ac8 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/open-sse/config/providers/registry/openai/index.ts b/open-sse/config/providers/registry/openai/index.ts
index 63b6a30fa37e..60a276a948b9 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/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts
index 54e30e573856..bf0c94ed90bc 100644
--- a/open-sse/config/providers/registry/opencode/go/index.ts
+++ b/open-sse/config/providers/registry/opencode/go/index.ts
@@ -126,6 +126,69 @@ export const opencode_goProvider: RegistryEntry = {
supportsReasoning: true,
},
{ id: "hy3-preview", name: "Hunyuan3 Preview" },
+ // Muse Spark 1.2 Contributor — base + effort-tier aliases from the OpenCode Go
+ // registry (`opencode models opencode-go --verbose`; exact suffix set:
+ // minimal/low/medium/high/xhigh, no max).
+ {
+ id: "muse-spark-1.2-contributor",
+ name: "Muse Spark 1.2 Contributor",
+ contextLength: 1048576,
+ maxOutputTokens: 131072,
+ supportsReasoning: true,
+ supportsVision: true,
+ supportsAudio: true,
+ supportsVideo: true,
+ },
+ {
+ id: "muse-spark-1.2-contributor-minimal",
+ name: "Muse Spark 1.2 Contributor (minimal effort)",
+ contextLength: 1048576,
+ maxOutputTokens: 131072,
+ supportsReasoning: true,
+ supportsVision: true,
+ supportsAudio: true,
+ supportsVideo: true,
+ },
+ {
+ id: "muse-spark-1.2-contributor-low",
+ name: "Muse Spark 1.2 Contributor (low effort)",
+ contextLength: 1048576,
+ maxOutputTokens: 131072,
+ supportsReasoning: true,
+ supportsVision: true,
+ supportsAudio: true,
+ supportsVideo: true,
+ },
+ {
+ id: "muse-spark-1.2-contributor-medium",
+ name: "Muse Spark 1.2 Contributor (medium effort)",
+ contextLength: 1048576,
+ maxOutputTokens: 131072,
+ supportsReasoning: true,
+ supportsVision: true,
+ supportsAudio: true,
+ supportsVideo: true,
+ },
+ {
+ id: "muse-spark-1.2-contributor-high",
+ name: "Muse Spark 1.2 Contributor (high effort)",
+ contextLength: 1048576,
+ maxOutputTokens: 131072,
+ supportsReasoning: true,
+ supportsVision: true,
+ supportsAudio: true,
+ supportsVideo: true,
+ },
+ {
+ id: "muse-spark-1.2-contributor-xhigh",
+ name: "Muse Spark 1.2 Contributor (xhigh effort)",
+ contextLength: 1048576,
+ maxOutputTokens: 131072,
+ supportsReasoning: true,
+ supportsVision: true,
+ supportsAudio: true,
+ supportsVideo: true,
+ },
// #8353: Grok 4.5 + effort tiers from the OpenCode Go registry.
{ id: "grok-4.5", name: "Grok 4.5", supportsReasoning: true },
{ id: "grok-4.5-low", name: "Grok 4.5 (low effort)", supportsReasoning: true },
diff --git a/open-sse/config/providers/registry/opencode/index.ts b/open-sse/config/providers/registry/opencode/index.ts
index 4aaa6ea046ea..07f228b77e83 100644
--- a/open-sse/config/providers/registry/opencode/index.ts
+++ b/open-sse/config/providers/registry/opencode/index.ts
@@ -22,6 +22,26 @@ export const opencodeProvider: RegistryEntry = {
supportsReasoning: true,
interleavedField: "reasoning_content",
},
+ // #MUSE_SPARK: Muse Spark is served by OpenCode Zen ONLY on the OpenAI
+ // Responses API (https://opencode.ai/zen/v1/responses), not /chat/completions
+ // (confirmed in the official OpenCode Zen docs: https://opencode.ai/docs/zen/).
+ // Without targetFormat:"openai-responses" these models fall through to the
+ // default chat/completions pass-through and the upstream returns null/empty
+ // content (see issue #10867). The opencode provider is passthrough, so
+ // declaring them here only sets the wire format / capability flags — the
+ // live upstream model list already advertises both ids.
+ {
+ id: "muse-spark-1.2",
+ name: "Muse Spark 1.2",
+ supportsReasoning: true,
+ targetFormat: "openai-responses",
+ },
+ {
+ id: "muse-spark-1.2-contributor-free",
+ name: "Muse Spark 1.2 Contributor Free",
+ supportsReasoning: true,
+ targetFormat: "openai-responses",
+ },
{ id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true },
// #6998: 2026-07-14 refresh — the upstream free tier rotated its lineup;
// minimax-m3-free, minimax-m2.5-free, ling-2.6-1t-free,
diff --git a/open-sse/config/providers/registry/sensenova/index.ts b/open-sse/config/providers/registry/sensenova/index.ts
index 37e95acc3202..2e8b5822f920 100644
--- a/open-sse/config/providers/registry/sensenova/index.ts
+++ b/open-sse/config/providers/registry/sensenova/index.ts
@@ -27,6 +27,8 @@ export const sensenovaProvider: RegistryEntry = {
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
+ supportedThinkingEfforts: ["none", "low", "medium", "high", "xhigh"],
+ supportsXHighEffort: true,
interleavedField: "reasoning_content",
},
{
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 000000000000..f95d188c20ec
--- /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/registry/token-kiosk/index.ts b/open-sse/config/providers/registry/token-kiosk/index.ts
new file mode 100644
index 000000000000..319747828cfc
--- /dev/null
+++ b/open-sse/config/providers/registry/token-kiosk/index.ts
@@ -0,0 +1,20 @@
+import type { RegistryEntry } from "../../shared.ts";
+
+export const token_kioskProvider: RegistryEntry = {
+ id: "token-kiosk",
+ alias: "tk",
+ format: "openai",
+ executor: "default",
+ baseUrl: "https://agent-router.gaib.ai/v1/chat/completions",
+ modelsUrl: "https://agent-router.gaib.ai/v1/models",
+ authType: "apikey",
+ authHeader: "bearer",
+ defaultContextLength: 128000,
+ models: [
+ { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet (Token Kiosk)", contextLength: 200000, toolCalling: true, supportsVision: true },
+ { id: "deepseek-v3", name: "DeepSeek V3 (Token Kiosk)", contextLength: 64000, toolCalling: true },
+ { id: "deepseek-r1", name: "DeepSeek R1 (Token Kiosk)", contextLength: 64000, toolCalling: true, supportsReasoning: true },
+ { id: "kimi-k1.5", name: "Kimi K1.5 (Token Kiosk)", contextLength: 128000, toolCalling: true },
+ { id: "minimax-m6", name: "MiniMax M6 (Token Kiosk)", contextLength: 128000, toolCalling: true },
+ ],
+};
diff --git a/open-sse/config/providers/registry/xai/index.ts b/open-sse/config/providers/registry/xai/index.ts
index efd0a72e3a41..dcf912444123 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/registry/zai/index.ts b/open-sse/config/providers/registry/zai/index.ts
index e126aee21f79..1141ea8dc399 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/config/providers/shared.ts b/open-sse/config/providers/shared.ts
index 88fff0fef732..d87250644dcb 100644
--- a/open-sse/config/providers/shared.ts
+++ b/open-sse/config/providers/shared.ts
@@ -46,6 +46,12 @@ export interface RegistryModel {
id: string;
name: string;
aliases?: readonly string[];
+ /**
+ * Upstream model IDs that prove this static model is live when the provider
+ * has an authoritative synchronized catalog. Needed for curated IDs whose
+ * public name differs from the ID sent to the upstream service.
+ */
+ liveCatalogIds?: readonly string[];
toolCalling?: boolean;
supportsReasoning?: boolean;
supportedThinkingEfforts?: readonly string[];
@@ -102,6 +108,8 @@ export interface RegistryOAuth {
pollUrlBase?: string;
}
+export type ReasoningTransport = "plaintext" | "opaque" | "none";
+
export interface RegistryEntry {
id: string;
alias?: string;
@@ -114,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. */
@@ -752,3 +762,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/config/searchRegistry.ts b/open-sse/config/searchRegistry.ts
index ce20777cb034..9230fa1b0e39 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/open-sse/executors/base.ts b/open-sse/executors/base.ts
index c91cb5cbb7b1..877431b68e5e 100644
--- a/open-sse/executors/base.ts
+++ b/open-sse/executors/base.ts
@@ -103,6 +103,7 @@ import {
} from "./base/headers.ts";
import { applyPeerTraceHeader } from "@/shared/resilience/peerRouting";
import { applyClineProtocolHeaders } from "@/shared/utils/clineAuth";
+import { isProbeContext } from "@/shared/utils/probeOrigin";
// Header helpers extracted to a pure leaf; re-exported for external importers
// (executors + tests) that import them from "./base.ts".
export {
@@ -689,7 +690,10 @@ export class BaseExecutor {
// Track per-URL intra-retry attempts to avoid infinite loops
const retryAttemptsByUrl: Record = {};
- if (this.needsRefresh(credentials)) {
+ // Probe-origin dispatches must not consume a refresh-token rotation —
+ // routing state untouched; the reactive 401/403 path is probe-guarded
+ // in chatCore (#9817).
+ if (!isProbeContext() && this.needsRefresh(credentials)) {
try {
// Fix A: wire onCredentialsRefreshed through runWithOnPersist so it runs
// INSIDE the per-connection mutex inside getAccessToken. Not every
@@ -800,7 +804,14 @@ export class BaseExecutor {
activeCredentials
);
const url = this.buildUrl(model, stream, urlIndex, requestCredentials);
- const headers = this.buildHeaders(requestCredentials, stream, clientHeaders, model, undefined, body);
+ const headers = this.buildHeaders(
+ requestCredentials,
+ stream,
+ clientHeaders,
+ model,
+ undefined,
+ body
+ );
applyConfiguredUserAgent(headers, requestCredentials?.providerSpecificData);
// Strip OpenAI SDK (X-Stainless-*) metadata + normalize SDK-derived User-Agent
diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts
index f3d138bb7985..9b8ffb1da036 100644
--- a/open-sse/executors/base/reasoningEffort.ts
+++ b/open-sse/executors/base/reasoningEffort.ts
@@ -6,6 +6,7 @@ import {
supportsClaudeMaxEffort,
supportsXHighEffort,
getProviderModel,
+ getProviderModels,
} from "../../config/providerModels.ts";
/**
@@ -351,6 +352,31 @@ export function sanitizeReasoningEffortForProvider(
// new models from being unusable for weeks until they're whitelisted (#8057).
if (effortStr === "max") {
if (supportsMax) return body; // explicitly known to accept max
+
+ // A model that explicitly advertises its accepted tiers is safe to normalize.
+ // Keep the default pass-through for absent metadata: an unlisted model might
+ // support literal `max`, and #8057 deliberately avoids blocking such models.
+ const providerModelId = modelStr.startsWith(`${provider}/`)
+ ? modelStr.slice(provider.length + 1)
+ : modelStr;
+ // Do not fall back to a globally registered model here. Identical ids can
+ // have different upstream contracts across providers (for example, OpenCode
+ // and SenseNova both expose deepseek-v4-flash with different max support).
+ const explicitEfforts = getProviderModels(provider).find(
+ (entry) => entry.id === providerModelId || entry.aliases?.includes(providerModelId)
+ )?.supportedThinkingEfforts;
+ const maxFallback =
+ Array.isArray(explicitEfforts) && !explicitEfforts.includes("max")
+ ? ["xhigh", "high", "medium", "low"].find((tier) => explicitEfforts.includes(tier))
+ : undefined;
+ if (maxFallback) {
+ log?.info?.(
+ "REASONING_SANITIZE",
+ `${provider}/${modelStr}: downgraded reasoning_effort max → ${maxFallback} (explicit model capability)`
+ );
+ return writeEffortValue(b, maxFallback, c);
+ }
+
if (!supportsXHigh) {
// Model is explicitly flagged as rejecting xhigh (and not in supportsMax) —
// it likely only accepts standard tiers. Degrade to its highest: high.
@@ -360,7 +386,6 @@ export function sanitizeReasoningEffortForProvider(
);
return writeEffortValue(b, "high", c);
}
- // Default: pass max through unchanged — trust the upstream
return body;
}
diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts
index 0168d07de2eb..438565b45cf1 100644
--- a/open-sse/executors/chatgpt-web.ts
+++ b/open-sse/executors/chatgpt-web.ts
@@ -32,7 +32,11 @@ import {
__resetChatGptImageCacheForTesting,
type ChatGptImageConversationContext,
} from "../services/chatgptImageCache.ts";
-import { isThinkingCapableModel, resolveChatGptModel } from "./chatgpt-web/models.ts";
+import {
+ resolveChatGptModel,
+ resolveChatGptSystemHints,
+ type ChatGptThinkingEffort,
+} from "./chatgpt-web/models.ts";
import { cleanChatGptText } from "./chatgpt-web/citations.ts";
import { resumeChatGptHandoff, type FinalAssistantAnswer } from "./chatgpt-web/handoff.ts";
@@ -43,7 +47,6 @@ const SESSION_URL = `${CHATGPT_BASE}/api/auth/session`;
const SENTINEL_PREPARE_URL = `${CHATGPT_BASE}/backend-api/sentinel/chat-requirements/prepare`;
const SENTINEL_CR_URL = `${CHATGPT_BASE}/backend-api/sentinel/chat-requirements`;
const CONV_URL = `${CHATGPT_BASE}/backend-api/f/conversation`;
-const USER_LAST_USED_MODEL_CONFIG_URL = `${CHATGPT_BASE}/backend-api/settings/user_last_used_model_config`;
const DEFAULT_PRO_POLL_TIMEOUT_MS = 20 * 60_000;
const DEFAULT_PRO_POLL_INTERVAL_MS = 4_000;
@@ -81,10 +84,8 @@ function deviceIdFor(cookie: string): string {
return id;
}
-// OmniRoute model ID → ChatGPT internal slug. The public ChatGPT Web catalog
-// keeps OmniRoute's historical dot-form IDs (e.g. "gpt-5.5-pro"), while
-// ChatGPT's backend routes use dash-form slugs (e.g. "gpt-5-5-pro"). The slug
-// catalog comes from /backend-api/models on a logged-in account.
+// OmniRoute model IDs select a GPT-5.6 Sol performance lane. Captured browser
+// requests use one of `gpt-5-6`, `gpt-5-6-thinking`, or `gpt-5-6-pro`.
// ─── Browser-like default headers ──────────────────────────────────────────
@@ -408,25 +409,6 @@ async function runSessionWarmup(
}
}
-// ─── Thinking-effort preference (PATCH user_last_used_model_config) ────────
-// chatgpt.com has two thinking levels for its dedicated thinking-models:
-// • standard — default, faster
-// • extended — longer reasoning budget
-// The browser sets the level by PATCHing `/backend-api/settings/user_last_used_model_config`
-// once, then issues the conversation request — the conversation endpoint itself
-// has no `thinking_effort` field; the server reads the user's stored preference
-// at routing time. We mirror that handshake when an OpenAI-style request
-// includes `reasoning_effort` (or a direct `providerSpecificData.thinkingEffort`
-// override).
-//
-// Cached per (cookie, slug, effort): the preference persists server-side, so
-// re-PATCHing the same combination is wasted bytes. Refreshed on TTL expiry or
-// whenever the caller switches efforts.
-
-const thinkingEffortCache = new Map();
-const THINKING_EFFORT_TTL_MS = 5 * 60 * 1000;
-const THINKING_EFFORT_CACHE_MAX = 400;
-
function configuredProPollTimeoutMs(): number {
const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS);
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PRO_POLL_TIMEOUT_MS;
@@ -439,73 +421,6 @@ function configuredProPollIntervalMs(): number {
return Math.floor(raw);
}
-async function setUserThinkingEffort(
- modelSlug: string,
- effort: "standard" | "extended" | "max",
- accessToken: string,
- accountId: string | null,
- sessionId: string,
- deviceId: string,
- cookie: string,
- signal: AbortSignal | null | undefined,
- log:
- | {
- debug?: (tag: string, msg: string) => void;
- warn?: (tag: string, msg: string) => void;
- }
- | null
- | undefined
-): Promise {
- const cacheKey = `${cookieKey(cookie)}:${modelSlug}:${effort}`;
- const now = Date.now();
- const last = thinkingEffortCache.get(cacheKey);
- if (last && now - last < THINKING_EFFORT_TTL_MS) {
- log?.debug?.("CGPT-WEB", `thinking_effort cached (${modelSlug}=${effort}) — skip PATCH`);
- return;
- }
- if (thinkingEffortCache.size >= THINKING_EFFORT_CACHE_MAX && !thinkingEffortCache.has(cacheKey)) {
- const first = thinkingEffortCache.keys().next().value;
- if (first) thinkingEffortCache.delete(first);
- }
-
- const url =
- `${USER_LAST_USED_MODEL_CONFIG_URL}` +
- `?model_slug=${encodeURIComponent(modelSlug)}` +
- `&thinking_effort=${encodeURIComponent(effort)}`;
- const headers: Record = {
- ...browserHeaders(),
- ...oaiHeaders(sessionId, deviceId),
- Accept: "application/json",
- Authorization: `Bearer ${accessToken}`,
- Cookie: buildSessionCookieHeader(cookie),
- Priority: "u=4",
- };
- if (accountId) headers["chatgpt-account-id"] = accountId;
-
- try {
- const r = await tlsFetchChatGpt(url, {
- method: "PATCH",
- headers,
- timeoutMs: 15_000,
- signal,
- });
- if (r.status >= 400) {
- log?.warn?.(
- "CGPT-WEB",
- `thinking_effort PATCH ${r.status} for ${modelSlug}=${effort} (continuing)`
- );
- return;
- }
- thinkingEffortCache.set(cacheKey, now);
- log?.debug?.("CGPT-WEB", `thinking_effort PATCH OK (${modelSlug}=${effort})`);
- } catch (err) {
- log?.warn?.(
- "CGPT-WEB",
- `thinking_effort PATCH failed: ${err instanceof Error ? err.message : String(err)}`
- );
- }
-}
-
async function prepareChatRequirements(
accessToken: string,
accountId: string | null,
@@ -889,6 +804,7 @@ interface ChatGptMessage {
id: string;
author: { role: string };
content: { content_type: "text"; parts: string[] };
+ metadata?: Record;
}
/**
@@ -984,7 +900,8 @@ function buildConversationBody(
// chatgpt.com history. Disable Temporary Chat only when ChatGPT needs a
// durable image conversation (image generation/editing).
persistConversation: boolean;
- thinkingEffort: "standard" | "extended" | "max" | null;
+ thinkingEffort: ChatGptThinkingEffort | null;
+ systemHints: readonly string[];
continuation?: ChatGptImageConversationContext | null;
}
): Record {
@@ -1021,6 +938,8 @@ function buildConversationBody(
});
}
+ const systemHints = options.systemHints;
+
const currentUserContent = hasOpenWebUIImageContext(parsed)
? "Briefly acknowledge the image result described in the system context. Do not generate, edit, or request another image."
: parsed.currentMsg || "";
@@ -1029,6 +948,7 @@ function buildConversationBody(
id: randomUUID(),
author: { role: "user" },
content: { content_type: "text", parts: [currentUserContent] },
+ ...(systemHints.length > 0 ? { metadata: { system_hints: [...systemHints] } } : {}),
});
return {
@@ -1051,6 +971,7 @@ function buildConversationBody(
supports_buffering: true,
force_parallel_switch: "auto",
paragen_cot_summary_display_override: "allow",
+ ...(systemHints.length > 0 ? { system_hints: [...systemHints] } : {}),
...(options.thinkingEffort ? { thinking_effort: options.thinkingEffort } : {}),
};
}
@@ -2920,24 +2841,6 @@ export class ChatGptWebExecutor extends BaseExecutor {
log
);
- // 2a''. Apply thinking-effort preference for thinking models.
- // Dedicated thinking models mirror the browser's user-config PATCH;
- // GPT-5.5 Pro effort is sent with the conversation body.
- const requestedEffort = resolvedModel.effort;
- if (requestedEffort && isThinkingCapableModel(model, modelSlug)) {
- await setUserThinkingEffort(
- modelSlug,
- requestedEffort,
- tokenEntry.accessToken,
- tokenEntry.accountId,
- sessionId,
- deviceId,
- cookie,
- signal,
- log
- );
- }
-
// 2b. Sentinel chat-requirements
let reqs: ChatRequirements;
try {
@@ -3019,7 +2922,7 @@ export class ChatGptWebExecutor extends BaseExecutor {
}
// Toggle Temporary Chat off only when ChatGPT needs a durable image
- // conversation. Text requests, including GPT-5.5 Pro, stay temporary so
+ // conversation. Text requests, including GPT-5.6 Sol Pro, stay temporary so
// they do not show up in the user's chatgpt.com sidebar/history.
const imageEdit = looksLikeImageEditRequest(parsed);
const continuation = imageEdit ? parsed.latestImageContext : null;
@@ -3033,13 +2936,14 @@ export class ChatGptWebExecutor extends BaseExecutor {
: "Image-gen intent detected — disabling Temporary Chat for this turn"
);
} else if (resolvedModel.isPro) {
- log?.debug?.("CGPT-WEB", "GPT-5.5 Pro text request — keeping Temporary Chat enabled");
+ log?.debug?.("CGPT-WEB", "GPT-5.6 Sol Pro text request — keeping Temporary Chat enabled");
}
const parentMessageId = continuation?.parentMessageId ?? randomUUID();
const cgptBody = buildConversationBody(parsed, modelSlug, parentMessageId, {
persistConversation,
- thinkingEffort: requestedEffort,
+ thinkingEffort: resolvedModel.effort,
+ systemHints: resolveChatGptSystemHints(model),
continuation,
});
@@ -3230,7 +3134,6 @@ function stringToStream(text: string): ReadableStream {
export function __resetChatGptWebCachesForTesting(): void {
tokenCache.clear();
warmupCache.clear();
- thinkingEffortCache.clear();
deviceIdCache.clear();
__resetChatGptImageCacheForTesting();
dplCache = null;
diff --git a/open-sse/executors/chatgpt-web/models.ts b/open-sse/executors/chatgpt-web/models.ts
index 87abd04880dc..1917437baaa7 100644
--- a/open-sse/executors/chatgpt-web/models.ts
+++ b/open-sse/executors/chatgpt-web/models.ts
@@ -3,106 +3,60 @@
export const MODEL_MAP: Record = {
// ChatGPT backend slugs are also accepted directly for power users / tests.
- "gpt-5-6-pro": "gpt-5-6-pro",
+ "gpt-5-6": "gpt-5-6",
"gpt-5-6-thinking": "gpt-5-6-thinking",
- "gpt-5-5-pro": "gpt-5-5-pro",
- "gpt-5-5-pro-extended": "gpt-5-5-pro",
- "gpt-5-5-thinking": "gpt-5-5-thinking",
+ "gpt-5-6-pro": "gpt-5-6-pro",
"gpt-5-5": "gpt-5-5",
- "gpt-5-3": "gpt-5-3",
- "gpt-5-3-mini": "gpt-5-3-mini",
+ "gpt-5-5-thinking": "gpt-5-5-thinking",
+ "gpt-5-5-pro": "gpt-5-5-pro",
+
+ // Free accounts leave Luna selection to ChatGPT's server-side auto router.
+ "gpt-5.6-luna-free": "auto",
+ "gpt-5.6-luna-free-thinking": "auto",
- // Public OmniRoute dot-form ids exposed by the provider catalog.
- "gpt-5.6-pro": "gpt-5-6-pro",
- "gpt-5.6-thinking": "gpt-5-6-thinking",
+ // Captured from a real ChatGPT v2 picker conversation. The visible
+ // performance levels select distinct backend model/effort pairs.
+ "gpt-5.6-sol-instant": "gpt-5-6",
+ "gpt-5.6-sol-medium": "gpt-5-6-thinking",
+ "gpt-5.6-sol-high": "gpt-5-6-thinking",
+ "gpt-5.6-sol-xhigh": "gpt-5-6-thinking",
+ "gpt-5.6-sol-pro": "gpt-5-6-pro",
+
+ "gpt-5.5-instant": "gpt-5-5",
+ "gpt-5.5-medium": "gpt-5-5-thinking",
+ "gpt-5.5-high": "gpt-5-5-thinking",
+ "gpt-5.5-xhigh": "gpt-5-5-thinking",
"gpt-5.5-pro": "gpt-5-5-pro",
"gpt-5.5-pro-extended": "gpt-5-5-pro",
- "gpt-5.5-thinking": "gpt-5-5-thinking",
+ // Compatibility alias for existing chatgpt-web image integrations. It is
+ // intentionally absent from the provider's visible curated model list.
"gpt-5.5": "gpt-5-5",
- "gpt-5.3-instant": "gpt-5-3-instant",
- "gpt-5.3": "gpt-5-3",
- "gpt-5.3-mini": "gpt-5-3-mini",
- o3: "o3",
};
export type ChatGptThinkingEffort = "standard" | "extended" | "max";
-export const MODEL_FORCED_EFFORT: Record = {
- "gpt-5-6-pro": "standard",
- "gpt-5.6-pro": "standard",
- "gpt-5-5-pro": "standard",
- "gpt-5-5-pro-extended": "extended",
+export const MODEL_FORCED_EFFORT: Record = {
+ "gpt-5.6-sol-instant": null,
+ "gpt-5.6-sol-medium": "standard",
+ "gpt-5.6-sol-high": "extended",
+ "gpt-5.6-sol-xhigh": "max",
+ "gpt-5.6-sol-pro": "standard",
+ "gpt-5.5-instant": null,
+ "gpt-5.5-medium": "standard",
+ "gpt-5.5-high": "extended",
+ "gpt-5.5-xhigh": "max",
"gpt-5.5-pro": "standard",
"gpt-5.5-pro-extended": "extended",
};
-/** Set of chatgpt.com slugs that the user_last_used_model_config endpoint
- * accepts a `thinking_effort` value for, derived from MODEL_MAP so adding a
- * new thinking entry there automatically extends this set.
- *
- * Derived from MODEL_MAP keys (always dot-form) that contain "thinking" or
- * are the `o3` reasoning model; the values are the chatgpt.com-side slugs. */
-export const THINKING_CAPABLE_SLUGS: ReadonlySet = new Set(
- Object.entries(MODEL_MAP)
- .filter(([k]) => k.includes("thinking") || k === "o3")
- .map(([, v]) => v)
-);
-
-/** chatgpt.com only exposes the thinking-effort toggle on dedicated thinking
- * models and the o-series. PATCHing for a non-thinking surface is a no-op
- * (the server accepts it but the routing-time read picks the wrong knob).
- *
- * The lookup also catches callers that pass a chatgpt.com slug directly as
- * the `model` field without MODEL_MAP translation. */
-export function isThinkingCapableModel(modelId: string, slug: string): boolean {
- return (
- modelId.includes("thinking") ||
- modelId === "o3" ||
- slug.includes("thinking") ||
- THINKING_CAPABLE_SLUGS.has(slug) ||
- THINKING_CAPABLE_SLUGS.has(modelId)
- );
-}
-
-/** Map either a chatgpt.com-native value (`standard`/`extended`/`max`) or the
- * OpenAI Chat Completions `reasoning_effort` field to the value the
- * `user_last_used_model_config` endpoint expects.
- *
- * minimal | low | medium | standard → standard
- * high | extended → extended
- * xhigh | max → max
- *
- * `xhigh` remains a compatibility alias for the highest ChatGPT Web tier.
- * Returns null for absent/unknown inputs. */
-export function normalizeThinkingEffort(input: unknown): ChatGptThinkingEffort | null {
- if (typeof input !== "string") return null;
- const v = input.trim().toLowerCase();
- if (v === "max" || v === "xhigh") return "max";
- if (v === "extended" || v === "high") return "extended";
- if (v === "standard" || v === "low" || v === "medium" || v === "minimal") {
- return "standard";
- }
- return null;
-}
+const MODEL_SYSTEM_HINTS: Record = {
+ // Captured from the Free-account Think toggle. ChatGPT sends this both at
+ // the request root and on the user message metadata.
+ "gpt-5.6-luna-free-thinking": ["reason"],
+};
-/** Resolve the requested effort for this turn.
- * Order: `providerSpecificData.thinkingEffort` (raw override, takes native
- * `standard`/`extended`/`max` values) > `body.reasoning_effort` (top-level
- * OpenAI Chat Completions field) > `body.reasoning.effort` (Responses-API
- * nesting). Returns null when the caller did not request one. */
-export function resolveThinkingEffort(
- body: unknown,
- providerSpecificData: Record | undefined
-): ChatGptThinkingEffort | null {
- if (providerSpecificData && providerSpecificData.thinkingEffort !== undefined) {
- return normalizeThinkingEffort(providerSpecificData.thinkingEffort);
- }
- const b = (body as Record | null) ?? null;
- if (!b) return null;
- const top = normalizeThinkingEffort(b.reasoning_effort);
- if (top) return top;
- const nested = (b.reasoning as Record | undefined)?.effort;
- return normalizeThinkingEffort(nested);
+export function resolveChatGptSystemHints(model: string): string[] {
+ return [...(MODEL_SYSTEM_HINTS[model] ?? [])];
}
export interface ResolvedChatGptModel {
@@ -113,12 +67,16 @@ export interface ResolvedChatGptModel {
export function resolveChatGptModel(
model: string,
- body: unknown,
- providerSpecificData: Record | undefined
+ _body?: unknown,
+ _providerSpecificData?: Record
): ResolvedChatGptModel {
const slug = MODEL_MAP[model] ?? model;
- const forcedEffort = MODEL_FORCED_EFFORT[model] ?? null;
- const effort = forcedEffort ?? resolveThinkingEffort(body, providerSpecificData);
- const isPro = slug === "gpt-5-6-pro" || slug === "gpt-5-5-pro";
+ const effort = MODEL_FORCED_EFFORT[model] ?? null;
+ const isPro =
+ model === "gpt-5.6-sol-pro" ||
+ model === "gpt-5.5-pro" ||
+ model === "gpt-5.5-pro-extended" ||
+ slug === "gpt-5-6-pro" ||
+ slug === "gpt-5-5-pro";
return { slug, effort, isPro };
}
diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts
index 93e5a2fe16e2..ab595c3cbb61 100644
--- a/open-sse/executors/codex.ts
+++ b/open-sse/executors/codex.ts
@@ -20,6 +20,7 @@ import {
import { FETCH_BODY_TIMEOUT_MS, HTTP_STATUS, PROVIDERS } from "../config/constants.ts";
import { readCodexPeekChunk, buildCodexTimeoutSafePassthroughBody } from "./codex/bodyTimeout.ts";
import {
+ CODEX_CLI_RS_ORIGINATOR,
getCodexClientVersion,
getCodexUserAgent,
normalizeCodexSessionId,
@@ -33,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";
@@ -41,8 +42,8 @@ import { errorResponse } from "../utils/error.ts";
import { normalizeCodexResponsesInput } from "../utils/responsesInputNormalization.ts";
import * as prl from "../utils/providerRequestLogging.ts";
import { createRequire } from "module";
-// Quota parsing/scheduling extracted to a pure leaf; re-exported for external
-// importers (handlers/chatCore/codexQuota.ts + tests).
+// Quota parsing/scheduling extracted to a pure leaf; re-exported for the
+// Codex account module and tests.
export {
type CodexQuotaSnapshot,
parseCodexQuotaHeaders,
@@ -225,7 +226,6 @@ function convertSystemToDeveloperRole(body: Record): void {
}
}
-
function stripOrphanedCodexFunctionCallOutputs(body: Record): void {
if (!Array.isArray(body.input)) return;
const input = body.input;
@@ -1045,10 +1045,11 @@ export class CodexExecutor extends BaseExecutor {
CodexClientIdentity | null | undefined;
const originalIdentityHeaders = credentials?.providerSpecificData
?.codexOriginalIdentityHeaders as Record | null | undefined;
+ const turnStateEcho = credentials?.providerSpecificData?.codexTurnStateEcho;
// Originator header — identifies the client type to the Codex backend.
// Ref: openai/codex login/src/auth/default_client.rs DEFAULT_ORIGINATOR = "codex_cli_rs"
- headers["originator"] = "codex_cli_rs";
+ headers["originator"] = CODEX_CLI_RS_ORIGINATOR;
// session_id header — enables prompt cache affinity on the Codex backend.
// The official Codex client sets this to conversation_id (a stable UUID per session).
@@ -1060,6 +1061,13 @@ export class CodexExecutor extends BaseExecutor {
applyCodexOriginalIdentityHeaders(headers, originalIdentityHeaders);
applyCodexClientIdentityHeaders(headers, clientIdentity);
+ // x-codex-turn-state: forward the client's echo when the provenance guard
+ // (in withCodexFingerprintCredentials) cleared it as same-account. The
+ // blob is account-bound; a stripped (absent) value must stay absent.
+ if (typeof turnStateEcho === "string" && turnStateEcho) {
+ headers["x-codex-turn-state"] = turnStateEcho;
+ }
+
return headers;
}
@@ -1381,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/executors/commandCode.ts b/open-sse/executors/commandCode.ts
index 6f7fe0894055..77779bb1f70c 100644
--- a/open-sse/executors/commandCode.ts
+++ b/open-sse/executors/commandCode.ts
@@ -387,6 +387,38 @@ const COMMAND_CODE_PASSTHROUGH_FIELDS = [
"extra_body",
] as const;
+/**
+ * Command Code's /alpha/generate endpoint serves most models under a
+ * vendor-prefixed wire id (e.g. `xiaomi/mimo-v2.5`, `deepseek/deepseek-v4-pro`,
+ * `moonshotai/Kimi-K2.6`) and defaults an unprefixed id to the `anthropic:`
+ * provider, which 403s with "Model/provider not recognized: anthropic:".
+ * The command-code registry ids already carry the vendor prefix, so a bare id
+ * reaching the executor is an operator-set custom model (e.g. the Vision Bridge
+ * picker, #10809). Map the small set of documented bare ids to their
+ * vendor-prefixed wire form; anything with an explicit `/` (or already wired)
+ * passes through untouched. Kept minimal and doc-backed, mirroring the
+ * `CC_VISION_MODEL_PATTERNS` philosophy.
+ */
+const COMMAND_CODE_BARE_MODEL_VENDOR_PREFIX: Readonly> = {
+ // Xiaomi MiMo V2.5 — the only CC-served vision model not in the registry.
+ "mimo-v2.5": "xiaomi/mimo-v2.5",
+ "mimo-v2.5-pro": "xiaomi/mimo-v2.5-pro",
+};
+
+/**
+ * Normalize an incoming model id to the wire form Command Code's upstream
+ * accepts. Strips a leading provider prefix (`command-code/` / `cmd/`) that the
+ * pipeline may have resolved, then maps known bare ids to their
+ * vendor-prefixed form (see above).
+ */
+function normalizeCommandCodeWireModel(model: string): string {
+ const trimmed = String(model || "").trim();
+ if (!trimmed) return trimmed;
+ const bare = trimmed.replace(/^(?:command-code|cmd)\//, "");
+ if (bare.includes("/")) return bare;
+ return COMMAND_CODE_BARE_MODEL_VENDOR_PREFIX[bare] ?? bare;
+}
+
function buildCommandCodeBody(
model: string,
body: unknown,
@@ -398,8 +430,10 @@ function buildCommandCodeBody(
// Payload rules may rewrite `body.model` (e.g. deepseek-v4-pro-max →
// deepseek/deepseek-v4-pro for the command-code provider). Prefer the
// rewritten value if present; fall back to the resolved combo model arg.
- const resolvedModel =
- typeof input.model === "string" && input.model.trim().length > 0 ? input.model : model;
+ // Normalize to the vendor-prefixed wire id the upstream requires (#10809).
+ const resolvedModel = normalizeCommandCodeWireModel(
+ typeof input.model === "string" && input.model.trim().length > 0 ? input.model : model
+ );
const converted = convertMessages(input.messages, resolvedModel, toolNameMap);
const explicitSystem = typeof input.system === "string" ? input.system : "";
diff --git a/open-sse/executors/copilot-m365-connection.ts b/open-sse/executors/copilot-m365-connection.ts
index 0c5303c250a2..c8251af5f4ca 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";
@@ -160,7 +161,16 @@ export function resolveConnectionParams(
const psd = (credentials?.providerSpecificData ?? {}) as JsonRecord;
const parsedApiKey =
typeof credentials?.apiKey === "string" ? parsePastedCredential(credentials.apiKey) : {};
+ // A JWT in credentials.accessToken (3 dot-separated parts — the individual-tier
+ // token is an opaque JWE with 5) is the freshest copy: the executor refreshes it
+ // in place before resolving params, and the framework mutates it after a refresh.
+ const credentialsJwt =
+ typeof credentials?.accessToken === "string" &&
+ credentials.accessToken.split(".").length === 3
+ ? credentials.accessToken
+ : "";
const accessToken =
+ credentialsJwt ||
parsedApiKey.accessToken ||
(typeof credentials?.apiKey === "string" &&
credentials.apiKey &&
@@ -254,6 +264,135 @@ export function redactWsUrl(wsUrl: string): string {
return wsUrl.replace(/access_token=[^&]*/i, "access_token=REDACTED");
}
+// ── OAuth refresh support (#10718 — client ids observed in the browser token
+// and M365-Copilot2API) ────────────────────────────────────────────────────
+//
+// The browser-issued access_token lives ~75 minutes. These helpers redeem a
+// stored refresh_token at the Microsoft identity platform (same public client
+// the m365.cloud.microsoft web app uses) so the connection self-heals instead
+// 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 = resolvePublicCred("m365_oauth_client_id");
+
+export const M365_OAUTH_SCOPE =
+ "openid profile offline_access https://substrate.office.com/sydney/M365Chat.Read " +
+ "https://substrate.office.com/sydney/sydney.readwrite";
+
+/** Refresh lead time — refresh when the current token has less than this left. */
+export const M365_REFRESH_LEAD_MS = 5 * 60 * 1000;
+
+type MinimalLog = {
+ info?: (tag: string, message: string) => void;
+ warn?: (tag: string, message: string) => void;
+};
+
+/** Decode a JWT payload WITHOUT verification — exp/tid are routing hints, never authz. */
+export function decodeJwtClaims(
+ token: string
+): { exp?: number; tid?: string; oid?: string } | null {
+ try {
+ const parts = token.split(".");
+ if (parts.length !== 3) return null;
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
+ return payload && typeof payload === "object" ? payload : null;
+ } catch {
+ return null;
+ }
+}
+
+/** True when the token is unreadable, already expired, or inside the refresh lead window. */
+export function tokenNeedsRefresh(token: string, leadMs = M365_REFRESH_LEAD_MS): boolean {
+ const claims = decodeJwtClaims(token);
+ if (!claims?.exp) return true;
+ return claims.exp * 1000 <= Date.now() + leadMs;
+}
+
+/** The freshest readable access token for a connection (JWT column → apiKey → psd). */
+export function currentM365AccessToken(
+ credentials: ProviderCredentials | undefined
+): string {
+ if (
+ typeof credentials?.accessToken === "string" &&
+ credentials.accessToken.split(".").length === 3
+ ) {
+ return credentials.accessToken;
+ }
+ if (typeof credentials?.apiKey === "string") {
+ const parsed = parsePastedCredential(credentials.apiKey);
+ if (parsed.accessToken && parsed.accessToken.split(".").length === 3) return parsed.accessToken;
+ // Opaque (JWE) individual-tier token — still a usable credential, just not refreshable.
+ return parsed.accessToken || "";
+ }
+ const psd = (credentials?.providerSpecificData ?? {}) as JsonRecord;
+ if (typeof psd.accessToken === "string") return psd.accessToken;
+ if (typeof psd.access_token === "string") return psd.access_token;
+ return "";
+}
+
+/** The chathub path (`@`) from wherever it is stored. */
+export function currentM365ChathubPath(credentials: ProviderCredentials | undefined): string {
+ const psd = (credentials?.providerSpecificData ?? {}) as JsonRecord;
+ return (
+ (typeof credentials?.apiKey === "string"
+ ? parsePastedCredential(credentials.apiKey).chathubPath
+ : "") ||
+ (typeof psd.chathubPath === "string" && psd.chathubPath) ||
+ (typeof psd.userTenant === "string" && psd.userTenant) ||
+ ""
+ );
+}
+
+export interface M365RefreshResult {
+ accessToken: string;
+ refreshToken?: string;
+ expiresIn?: number;
+}
+
+/**
+ * Redeem the refresh_token (public client — no secret). MS may rotate the
+ * refresh_token; callers MUST persist the returned one when present or the
+ * token family dies after the first refresh.
+ */
+export async function refreshM365AccessToken(
+ refreshToken: string,
+ tid: string,
+ log?: MinimalLog
+): Promise {
+ const endpoint = `https://login.microsoftonline.com/${tid || "common"}/oauth2/v2.0/token`;
+ try {
+ const res = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Accept: "application/json",
+ },
+ body: new URLSearchParams({
+ client_id: M365_OAUTH_CLIENT_ID,
+ grant_type: "refresh_token",
+ refresh_token: refreshToken,
+ scope: M365_OAUTH_SCOPE,
+ }),
+ });
+ const data = (await res.json().catch(() => ({}))) as Record;
+ if (!res.ok || typeof data.access_token !== "string") {
+ const error = typeof data.error === "string" ? data.error : `HTTP ${res.status}`;
+ log?.warn?.("M365_TOKEN", `refresh_token grant failed: ${error}`);
+ return { error };
+ }
+ log?.info?.("M365_TOKEN", "access token refreshed via refresh_token grant");
+ return {
+ accessToken: data.access_token,
+ refreshToken: typeof data.refresh_token === "string" ? data.refresh_token : undefined,
+ expiresIn: typeof data.expires_in === "number" ? data.expires_in : undefined,
+ };
+ } catch (e) {
+ const error = e instanceof Error ? e.message : String(e);
+ log?.warn?.("M365_TOKEN", `refresh request failed: ${error}`);
+ return { error };
+ }
+}
+
/** Flatten OpenAI messages into a single prompt (system instructions prepended). */
export function buildPrompt(body: JsonRecord | undefined): string {
const messages = (body?.messages as Array) || [];
diff --git a/open-sse/executors/copilot-m365-frames.ts b/open-sse/executors/copilot-m365-frames.ts
index add2716ee081..c8c6dee7be08 100644
--- a/open-sse/executors/copilot-m365-frames.ts
+++ b/open-sse/executors/copilot-m365-frames.ts
@@ -11,7 +11,9 @@
* Protocol (from @skyzea1's #4042 capture):
* - JSON messages terminated with the SignalR record separator `\x1e`.
* - Handshake: → {"protocol":"json","version":1} ← {} → {"type":6}
- * - Send: type:4 invocation to target "chat" with arguments[0] = { message, ... }
+ * - Send: type:4 invocation to target "chat" with arguments[0] = { message, ... },
+ * immediately followed by a type:1 target:"Metrics" frame in the SAME socket
+ * write (#10718 — an invocation without its Metrics pair is silently dropped).
* - Stream: type:1 target:"update" deltas (bot text at arguments[0].messages[].text,
* accumulated — NOT incremental) → isLastUpdate:true → type:2 final → type:3 completion.
*/
@@ -25,19 +27,18 @@ export const HANDSHAKE_REQUEST = { protocol: "json", version: 1 } as const;
/** SignalR keepalive ping frame. */
export const KEEPALIVE_PING = { type: 6 } as const;
-/** Allowed message types observed in the individual M365 send frame. */
+/**
+ * Allowed message types observed in the 2026-08 recapture of the working
+ * `m365.cloud.microsoft/chat` client (#10718). The old 11-entry list is no longer
+ * seen on the wire — the stale shape gets closed immediately after the type:4.
+ */
export const ALLOWED_MESSAGE_TYPES = [
"Chat",
"Suggestion",
- "InternalSearchQuery",
"Disengaged",
- "InternalLoaderMessage",
"Progress",
- "GeneratedCode",
- "RenderCardRequest",
- "AdsQuery",
- "SemanticSerp",
- "GenerateContentQuery",
+ "EndOfRequest",
+ "InternalLoaderMessage",
] as const;
/**
@@ -74,22 +75,20 @@ export const M365_ENTERPRISE_EXTRA_MESSAGE_TYPES = [
"SwitchRespondingEndpoint",
] as const;
+/**
+ * Individual / EDU option sets from the 2026-08 recapture (#10718) — 14 entries.
+ * The previous 25-entry consumer/MSA set (enable_msa_user, pdnascan, cwc_code_*,
+ * …) is no longer observed on the wire and belongs to the shape the substrate
+ * now drops silently.
+ */
export const M365_DEFAULT_OPTION_SETS = [
"search_result_progress_messages_with_search_queries",
"update_textdoc_response_after_streaming",
"deepleo_networking_timeout_10minutes_canmore",
"cwc_flux_image",
- "cwc_code_interpreter",
- "cwc_code_interpreter_amsfix",
- "enable_msa_user",
- "cwcgptv",
+ "cwcfluxgptv",
"flux_v3_gptv_enable_upload_multi_image_in_turn_wo_ch",
"gptvnorm2048",
- "pdnascan",
- "cwc_code_interpreter_citation_fix",
- "code_interpreter_interactive_charts",
- "cwc_code_interpreter_interactive_charts_inline_image",
- "code_interpreter_matplotlib_patching",
"cwc_fileupload_odb",
"update_memory_plugin",
"add_custom_instructions",
@@ -97,9 +96,6 @@ export const M365_DEFAULT_OPTION_SETS = [
"flux_v3_progress_messages",
"enable_batch_token_processing",
"enable_gg_gpt",
- "flux_v3_image_gen_enable_non_watermarked_storage",
- "flux_v3_image_gen_enable_story",
- "rich_responses",
] as const;
/** Append the record separator to a JSON-serializable frame. */
@@ -117,6 +113,32 @@ export function keepaliveFrame(): string {
return encodeFrame(KEEPALIVE_PING);
}
+/**
+ * #10718 — the browser follows the type:4 chat invocation with this type:1
+ * target:"Metrics" frame in the SAME socket write. Sending the invocation alone
+ * gets it silently ignored (no update frames at all), so the executor must
+ * concatenate `metricsFrame()` onto the invocation payload.
+ */
+export const CHAT_METRICS_FRAME = {
+ arguments: [
+ {
+ Timestamps: {
+ ConnectionEstablished: "",
+ ConnectionStart: "",
+ UserInputStart: "",
+ UserInputSubmit: "",
+ },
+ },
+ ],
+ target: "Metrics",
+ type: 1,
+} as const;
+
+/** Serialized Metrics follow-up frame (see {@link CHAT_METRICS_FRAME}). */
+export function metricsFrame(): string {
+ return encodeFrame(CHAT_METRICS_FRAME);
+}
+
/**
* Split a raw socket buffer into complete `\x1e`-terminated frames, returning any
* trailing partial frame as `rest` so it can be prepended to the next chunk.
@@ -155,22 +177,37 @@ export function handshakeError(frame: Record | null): string |
export interface ChatInvocationOptions {
text: string;
- /** Per-connection trace id (hex), reused as clientCorrelationId/traceId. */
+ /** Per-invocation trace id (GUID). */
traceId: string;
- /** Per-session id (GUID). */
+ /** Client correlation id; defaults to {@link ChatInvocationOptions.traceId}. */
+ clientCorrelationId?: string;
+ /** Per-session id (GUID, == the WS URL X-SessionId query). */
sessionId: string;
+ /** Per-request id (== the WS URL chatsessionid/clientrequestid query). */
+ requestId: string;
+ /**
+ * Conversation id — MUST match the ConversationId query of the WS URL the
+ * invocation rides on (#10718: the server cross-checks the two).
+ */
+ conversationId: string;
+ /** BCP-47 locale echoed in message.locale; defaults to "en-us". */
+ locale?: string;
+ /** IANA time zone for message.locationInfo; defaults to "UTC". */
+ timeZone?: string;
+ /** Hour offset for message.locationInfo; defaults to 0. */
+ timeZoneOffset?: number;
/** Whether this is the first turn of the conversation. */
isStartOfSession?: boolean;
- /** Tier-specific option flags; left empty by default (tuned during live validation). */
+ /** Tier-specific option flags; defaults to {@link M365_DEFAULT_OPTION_SETS}. */
optionsSets?: string[];
tone?: string;
/** Tier-specific allowed message types; defaults to {@link ALLOWED_MESSAGE_TYPES}. */
allowedMessageTypes?: readonly string[];
/**
- * Tier-specific disconnect behavior sent in every type:4 chat invocation. The work
- * Surface rejects any value other than exactly "continue" (#8971). Defaults to ""
- * for individual/consumer/EDU tiers; {@link resolveChatInvocationOverrides} returns
- * "continue" for the enterprise tier.
+ * Tier-specific disconnect behavior sent in the type:4 chat invocation. The work
+ * surface rejects any value other than exactly "continue" (#8971), so the
+ * enterprise tier sends it; the 2026-08 recapture shows the individual/EDU
+ * surface omits the key entirely, so it is left out unless set (#10718).
*/
disconnectBehavior?: string;
}
@@ -185,7 +222,7 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
optionsSets: string[];
tone: string;
allowedMessageTypes: readonly string[];
- disconnectBehavior: string;
+ disconnectBehavior: string | undefined;
} {
if (tier === "enterprise") {
return {
@@ -197,9 +234,12 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
}
return {
optionsSets: [...M365_DEFAULT_OPTION_SETS],
- tone: "",
+ // #10718 — the 2026-08 recapture sends tone:"magic" (lowercase) on the
+ // individual/EDU surface; the old "" default is part of the dropped shape.
+ tone: "magic",
allowedMessageTypes: ALLOWED_MESSAGE_TYPES,
- disconnectBehavior: "",
+ // Omitted entirely on the individual/EDU wire (see ChatInvocationOptions).
+ disconnectBehavior: undefined,
};
}
@@ -207,7 +247,7 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
* BizChat exposes several models selected by the `tone` field of the `type:4` chat
* invocation (#7872, values confirmed against a real enterprise tenant in #7850). Each
* tone-selected variant is registered as its own model id; the bare `copilot-m365` id is
- * intentionally absent here so it keeps the tier default tone (`Magic` on enterprise, `""`
+ * intentionally absent here so it keeps the tier default tone (`Magic` on enterprise, `magic`
* otherwise) resolved by {@link resolveChatInvocationOverrides}.
*/
export const M365_MODEL_TONE_MAP: Readonly> = {
@@ -228,7 +268,14 @@ export function resolveToneForModel(model: string | undefined): string | undefin
/**
* Build the `type:4` chat invocation frame body (not yet `\x1e`-terminated).
- * Mirrors the argument shape captured on the individual M365 path in #4042.
+ * Mirrors the argument shape recaptured from a working `m365.cloud.microsoft/chat`
+ * client in 2026-08 (#10718). Notable differences from the pre-#10718 shape: a
+ * populated `clientInfo` + `productThreadType:"Office"`, a `conversationId`
+ * matching the WS URL query, a rich `message` object, and no
+ * `spokenTextMode` / `extraExtensionParameters` / `isSbsSupported` /
+ * `renderReferencesBehindEOS` / `disconnectBehavior` — none of those are still
+ * observed on the wire, and the stale shape gets closed immediately after the
+ * invocation.
*/
export function buildChatInvocation(opts: ChatInvocationOptions): Record {
return {
@@ -237,33 +284,48 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record {
- ws?.send(keepaliveFrame());
const overrides = resolveChatInvocationOverrides(input.tier);
// Model-driven tone (#7872) wins over the tier default; a bare/unknown id
// keeps the tier tone resolved above.
const tone = resolveToneForModel(input.model) ?? overrides.tone;
- ws?.send(
- encodeFrame(
- buildChatInvocation({
- text: input.prompt,
- traceId,
- sessionId,
- isStartOfSession: true,
- ...overrides,
- tone,
- })
- )
+ const invocationFrame = encodeFrame(
+ buildChatInvocation({
+ text: input.prompt,
+ traceId,
+ sessionId,
+ requestId,
+ conversationId,
+ isStartOfSession: true,
+ ...overrides,
+ tone,
+ })
);
+ // #10718 — the invocation and its type:1 Metrics follow-up must land
+ // in ONE socket write, exactly as the browser sends them; a bare
+ // invocation (or one preceded by a type:6 ping) is silently dropped.
+ ws?.send(invocationFrame + metricsFrame());
};
ws.on("open", () => {
@@ -273,6 +290,62 @@ export class CopilotM365WebExecutor extends BaseExecutor {
);
}
+ /**
+ * #10718 — proactively refresh the M365 access token before opening the WS.
+ * A WS-handshake 401 surfaces as an error event INSIDE the SSE stream (the HTTP
+ * response is already 200 by then), so chatCore's generic 401→refresh→retry
+ * orchestration never triggers — the refresh has to happen here, pre-flight.
+ * No-ops for legacy connections without a stored refresh_token.
+ */
+ private async ensureFreshCredentials(
+ credentials: ExecuteInput["credentials"],
+ onCredentialsRefreshed: ExecuteInput["onCredentialsRefreshed"],
+ log: ExecutorLog | null
+ ): Promise {
+ const psd = (credentials?.providerSpecificData ?? {}) as JsonRecord;
+ const refreshToken =
+ credentials.refreshToken || (typeof psd.refreshToken === "string" ? psd.refreshToken : "");
+ if (!refreshToken) return;
+
+ const current = currentM365AccessToken(credentials);
+ if (current && !tokenNeedsRefresh(current)) return;
+
+ const tid =
+ decodeJwtClaims(current)?.tid || (typeof psd.tid === "string" ? psd.tid : "") || "";
+ const result = await refreshM365AccessToken(refreshToken, tid, log ?? undefined);
+ if ("error" in result) {
+ // Fall through with the existing token — the WS layer will surface the failure.
+ return;
+ }
+
+ 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.
+ ...(assembledApiKey ? { apiKey: assembledApiKey } : {}),
+ ...(result.expiresIn
+ ? { expiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString() }
+ : {}),
+ };
+ Object.assign(credentials, next);
+ try {
+ await onCredentialsRefreshed?.(next);
+ } catch (err) {
+ // #7676 pattern: a persistence failure must never fail the user-facing response.
+ log?.warn?.(
+ "M365_TOKEN",
+ `persisting refreshed token failed (${err instanceof Error ? err.message : String(err)}) — will re-refresh next request`
+ );
+ }
+ }
+
async execute(input: ExecuteInput): Promise<{
response: Response;
url: string;
@@ -293,6 +366,12 @@ export class CopilotM365WebExecutor extends BaseExecutor {
};
}
+ await this.ensureFreshCredentials(
+ input.credentials,
+ input.onCredentialsRefreshed,
+ input.log ?? null
+ );
+
const connectionParams = resolveConnectionParams(input.credentials);
if ("error" in connectionParams) {
return {
diff --git a/open-sse/executors/cursor.ts b/open-sse/executors/cursor.ts
index 89f7799b03dc..ebcfe053bd2a 100644
--- a/open-sse/executors/cursor.ts
+++ b/open-sse/executors/cursor.ts
@@ -57,6 +57,13 @@ import {
type StreamingState as ComposerStreamingState,
} from "../utils/composerToolCalls.ts";
import { cursorSessionManager, type CursorSession } from "../services/cursorSessionManager.ts";
+import {
+ CursorApiKeyExchangeError,
+ invalidateCursorSessionToken,
+ isCursorApiKey,
+ resolveCursorBearerToken,
+ stripCursorOAuthTokenPrefix,
+} from "../services/cursorApiKeyAuth.ts";
import crypto from "crypto";
import * as fs from "node:fs";
import * as zlib from "node:zlib";
@@ -75,6 +82,7 @@ import {
visibleComposerContentFromThinking,
composerReasoningRemainder,
} from "./cursor/composer.ts";
+import { CursorServerConfigError, resolveCursorAgentUrl } from "./cursor/agentEndpoint.ts";
import { getActiveSyncedCatalog } from "../../src/lib/db/models/activeSyncedCatalog.ts";
// Composer helpers re-exported for external importers (tests).
export {
@@ -186,10 +194,6 @@ function buildExecRejection(event: ExecServerEvent): Buffer | null {
}
}
-const CURSOR_AGENT_HOST = "agentn.global.api5.cursor.sh";
-const CURSOR_AGENT_PATH = "/agent.v1.AgentService/Run";
-const CURSOR_AGENT_URL = `https://${CURSOR_AGENT_HOST}${CURSOR_AGENT_PATH}`;
-
// Detect cloud environment (Edge runtime, Cloudflare Workers, etc.)
const isCloudEnv = () => {
if (typeof caches !== "undefined" && typeof caches === "object") return true;
@@ -706,18 +710,44 @@ export function processFrame(
}
export class CursorExecutor extends BaseExecutor {
- constructor() {
- super("cursor", PROVIDERS.cursor);
+ constructor(provider: "cursor" | "cursor-api" = "cursor") {
+ super(provider, PROVIDERS[provider]);
}
buildUrl() {
- return CURSOR_AGENT_URL;
+ return PROVIDERS.cursor.baseUrl;
+ }
+
+ /**
+ * API-key connections carry a `crsr_…` key that api2.cursor.sh does not
+ * accept as a Bearer; swap it for the exchanged session token before the
+ * h2 stream is opened. OAuth/IDE-session connections pass through untouched.
+ */
+ async resolveExecutionCredentials(credentials) {
+ if (!isCursorApiKey(credentials?.apiKey)) return credentials;
+ try {
+ const accessToken = await resolveCursorBearerToken(credentials);
+ return { ...credentials, accessToken };
+ } catch (err) {
+ const status =
+ err instanceof CursorApiKeyExchangeError ? err.status : HTTP_STATUS.SERVER_ERROR;
+ const message = err instanceof Error ? err.message : String(err);
+ return new Response(
+ JSON.stringify({
+ error: {
+ message: sanitizeErrorMessage(message),
+ type: status === HTTP_STATUS.UNAUTHORIZED ? "authentication_error" : "connection_error",
+ code: "",
+ },
+ }),
+ { status, headers: { "Content-Type": "application/json" } }
+ );
+ }
}
buildHeaders(credentials) {
- const accessToken = credentials.accessToken;
const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
- const cleanToken = accessToken.includes("::") ? accessToken.split("::")[1] : accessToken;
+ const cleanToken = stripCursorOAuthTokenPrefix(credentials.accessToken ?? "");
const requestId = crypto.randomUUID();
const traceParent = `00-${crypto.randomBytes(16).toString("hex")}-${crypto.randomBytes(8).toString("hex")}-01`;
@@ -825,7 +855,7 @@ export class CursorExecutor extends BaseExecutor {
*/
private async loadLiveCatalogIds(): Promise | undefined> {
try {
- const catalog = await getActiveSyncedCatalog("cursor");
+ const catalog = await getActiveSyncedCatalog(this.provider);
if (!catalog.models.length) return undefined;
return new Set(catalog.models.map((model) => model.id));
} catch {
@@ -1178,8 +1208,42 @@ export class CursorExecutor extends BaseExecutor {
}
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) {
- const url = this.buildUrl();
- const headers = this.buildHeaders(credentials);
+ const fallbackUrl = this.buildUrl();
+ const executionCredentials = await this.resolveExecutionCredentials(credentials);
+ if (executionCredentials instanceof Response) {
+ return {
+ response: executionCredentials,
+ url: fallbackUrl,
+ headers: {},
+ transformedBody: body,
+ };
+ }
+ let url: string;
+ try {
+ url = await resolveCursorAgentUrl(executionCredentials, signal);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ const headers = this.buildHeaders(executionCredentials);
+ return {
+ response: new Response(
+ JSON.stringify({
+ error: {
+ message: sanitizeErrorMessage(message),
+ type: "connection_error",
+ code: "",
+ },
+ }),
+ {
+ status: err instanceof CursorServerConfigError ? err.status : HTTP_STATUS.SERVER_ERROR,
+ headers: { "Content-Type": "application/json" },
+ }
+ ),
+ url: fallbackUrl,
+ headers,
+ transformedBody: body,
+ };
+ }
+ const headers = this.buildHeaders(executionCredentials);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const messages: ChatMessage[] = body.messages || [];
@@ -1252,8 +1316,10 @@ export class CursorExecutor extends BaseExecutor {
if (isToolFollowUp) {
session = cursorSessionManager.acquire(conversationId);
// #9029: content-based session match when client lacks conversation_id.
- if (!session && !body.conversation_id) session = cursorSessionManager.findByToolCallIds(
- messages.filter(m => m.role === "tool" && m.tool_call_id).map(m => m.tool_call_id!));
+ if (!session && !body.conversation_id)
+ session = cursorSessionManager.findByToolCallIds(
+ messages.filter((m) => m.role === "tool" && m.tool_call_id).map((m) => m.tool_call_id!)
+ );
}
if (session) {
@@ -1334,6 +1400,9 @@ export class CursorExecutor extends BaseExecutor {
if (opened.status !== 200) {
const errBuf = await opened.consumeError();
const errText = errBuf.toString("utf8") || "Unknown error";
+ if (opened.status === HTTP_STATUS.UNAUTHORIZED && isCursorApiKey(credentials.apiKey)) {
+ invalidateCursorSessionToken(credentials.apiKey);
+ }
return {
response: buildErrorResponse(opened.status, `[${opened.status}]: ${errText}`),
url,
diff --git a/open-sse/executors/cursor/agentEndpoint.ts b/open-sse/executors/cursor/agentEndpoint.ts
new file mode 100644
index 000000000000..e1168df5b766
--- /dev/null
+++ b/open-sse/executors/cursor/agentEndpoint.ts
@@ -0,0 +1,121 @@
+import { createHmac } from "node:crypto";
+
+import { mergeAbortSignals, type ProviderCredentials } from "../base.ts";
+import { stripCursorOAuthTokenPrefix } from "../../services/cursorApiKeyAuth.ts";
+import {
+ formatCursorAgentClientVersion,
+ getCursorAgentCliVersion,
+} from "../../utils/cursorAgentCliVersion.ts";
+import { decodeFields } from "../../utils/cursorAgentProtobuf/wire.ts";
+
+const CURSOR_API_URL = "https://api2.cursor.sh";
+const CURSOR_SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
+const CURSOR_AGENT_PATH = "/agent.v1.AgentService/Run";
+const CURSOR_SERVER_CONFIG_TIMEOUT_MS = 10_000;
+const CURSOR_AGENT_URL_CACHE_TTL_MS = 60 * 60 * 1000;
+const CURSOR_AGENT_URL_CACHE_LIMIT = 1_000;
+
+type CursorAgentUrls = { agentUrl: string; agentnUrl: string };
+type CursorAgentUrlCacheEntry = CursorAgentUrls & { expiresAt: number };
+const cursorAgentUrlCache = new Map();
+
+/** Reports an HTTP error from Cursor server-config discovery. */
+export class CursorServerConfigError extends Error {
+ constructor(
+ message: string,
+ readonly status: number
+ ) {
+ super(message);
+ }
+}
+
+function validateCursorAgentUrl(value: string): string {
+ const url = new URL(value);
+ const isCursorAgentHost =
+ url.hostname === "api5.cursor.sh" || url.hostname.endsWith(".api5.cursor.sh");
+ if (
+ url.protocol !== "https:" ||
+ !isCursorAgentHost ||
+ url.username ||
+ url.password ||
+ url.search ||
+ url.hash
+ ) {
+ throw new Error("Cursor server config included an invalid Agent URL");
+ }
+ return url.origin;
+}
+
+function parseCursorAgentUrls(payload: Buffer): CursorAgentUrls {
+ const agentUrlConfig = decodeFields(payload).find(
+ (field) => field.fieldNumber === 27 && field.wireType === 2
+ );
+ if (!agentUrlConfig || agentUrlConfig.wireType !== 2) {
+ throw new Error("Cursor server config did not include Agent URLs");
+ }
+ const fields = decodeFields(agentUrlConfig.bytes);
+ const agentUrl = fields.find((field) => field.fieldNumber === 1 && field.wireType === 2);
+ const agentnUrl = fields.find((field) => field.fieldNumber === 2 && field.wireType === 2);
+ if (!agentUrl || agentUrl.wireType !== 2 || !agentnUrl || agentnUrl.wireType !== 2) {
+ throw new Error("Cursor server config included incomplete Agent URLs");
+ }
+ return {
+ agentUrl: validateCursorAgentUrl(agentUrl.bytes.toString("utf8")),
+ agentnUrl: validateCursorAgentUrl(agentnUrl.bytes.toString("utf8")),
+ };
+}
+
+async function fetchCursorAgentUrls(
+ accessToken: string,
+ signal?: AbortSignal | null
+): Promise {
+ const timeoutSignal = AbortSignal.timeout(CURSOR_SERVER_CONFIG_TIMEOUT_MS);
+ const response = await fetch(`${CURSOR_API_URL}${CURSOR_SERVER_CONFIG_PATH}`, {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${accessToken}`,
+ "connect-protocol-version": "1",
+ "content-type": "application/proto",
+ "user-agent": "connect-es/1.6.1",
+ "x-cursor-client-type": "cli",
+ "x-cursor-client-version": formatCursorAgentClientVersion(getCursorAgentCliVersion()),
+ },
+ body: Buffer.alloc(0),
+ signal: signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal,
+ });
+ if (!response.ok) {
+ throw new CursorServerConfigError(
+ `Cursor server config request failed with status ${response.status}`,
+ response.status
+ );
+ }
+ return parseCursorAgentUrls(Buffer.from(await response.arrayBuffer()));
+}
+
+/** Resolve the Agent RPC URL that Cursor assigned to this connection. */
+export async function resolveCursorAgentUrl(
+ credentials: ProviderCredentials,
+ signal?: AbortSignal | null
+): Promise {
+ const accessToken = stripCursorOAuthTokenPrefix(credentials.accessToken || "");
+ if (!accessToken) throw new Error("Cursor access token is required");
+ const cacheKey =
+ `${credentials.connectionId || "anonymous"}:` +
+ createHmac("sha256", "omniroute-cursor-agent-url-cache-v1").update(accessToken).digest("hex");
+ const now = Date.now();
+ let urls = cursorAgentUrlCache.get(cacheKey);
+ if (!urls || urls.expiresAt <= now) {
+ const fetched = await fetchCursorAgentUrls(accessToken, signal);
+ urls = { ...fetched, expiresAt: now + CURSOR_AGENT_URL_CACHE_TTL_MS };
+ if (
+ !cursorAgentUrlCache.has(cacheKey) &&
+ cursorAgentUrlCache.size >= CURSOR_AGENT_URL_CACHE_LIMIT
+ ) {
+ const oldestKey = cursorAgentUrlCache.keys().next().value as string | undefined;
+ if (oldestKey !== undefined) cursorAgentUrlCache.delete(oldestKey);
+ }
+ cursorAgentUrlCache.set(cacheKey, urls);
+ }
+ const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
+ return `${ghostMode ? urls.agentUrl : urls.agentnUrl}${CURSOR_AGENT_PATH}`;
+}
diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts
index 056772040ac9..0d2c9182bd45 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/open-sse/executors/freebuff.ts b/open-sse/executors/freebuff.ts
new file mode 100644
index 000000000000..bc2de30f632f
--- /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/gitlab.ts b/open-sse/executors/gitlab.ts
index fa0b22c5c1a8..b82d5c47aa6e 100644
--- a/open-sse/executors/gitlab.ts
+++ b/open-sse/executors/gitlab.ts
@@ -10,6 +10,7 @@ import {
} from "./base.ts";
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { getAccessToken } from "../services/tokenRefresh.ts";
+import { isProbeContext } from "@/shared/utils/probeOrigin";
import { prepareToolMessages, buildToolAwareResult } from "../translator/webTools.ts";
import {
buildStreamingResponse,
@@ -208,9 +209,7 @@ function buildToolExchangePrompt(messages: OpenAIMessage[]): string {
const line = renderConversationTurn(message, role, text);
if (line) convo.push(line);
}
- const header = systemParts.length
- ? `System instructions:\n${systemParts.join("\n\n")}\n\n`
- : "";
+ const header = systemParts.length ? `System instructions:\n${systemParts.join("\n\n")}\n\n` : "";
const body = `${header}${convo.join(
"\n\n"
)}\n\nContinue the response using the tool result above; do not repeat the tool call.`.trim();
@@ -672,7 +671,9 @@ export class GitlabExecutor extends BaseExecutor {
}
let activeCredentials = input.credentials;
- if (this.needsRefresh(activeCredentials)) {
+ // Probe-origin dispatches must not consume a refresh-token rotation —
+ // routing state untouched; mirrors the base.ts guard (#9817).
+ if (!isProbeContext() && this.needsRefresh(activeCredentials)) {
const refreshed = await this.refreshCredentials(activeCredentials, input.log || null);
if (refreshed) {
activeCredentials = mergeCredentials(activeCredentials, refreshed);
diff --git a/open-sse/executors/glm.ts b/open-sse/executors/glm.ts
index 945dbe82cbe4..29384053a81f 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;
+ }
}
/**
@@ -244,8 +268,10 @@ export class GlmExecutor extends DefaultExecutor {
stream = true,
_clientHeaders?: Record | null,
_model?: string,
- transport: GlmTransport = getGlmTransport(credentials.providerSpecificData)
+ _health?: unknown,
+ _body?: unknown
): Record {
+ const transport: GlmTransport = getGlmTransport(credentials.providerSpecificData);
if (transport === "openai") {
return buildGlmCodingHeaders(getEffectiveKey(credentials), stream);
}
@@ -278,7 +304,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 +339,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 };
}
@@ -364,13 +398,7 @@ export class GlmExecutor extends DefaultExecutor {
): Promise {
const credentials = input.credentials;
const url = buildGlmChatUrl(credentials?.providerSpecificData, transport, this.config.baseUrl);
- const headers = this.buildHeaders(
- credentials,
- input.stream,
- input.clientHeaders,
- input.model,
- transport
- );
+ const headers = this.buildHeaders(credentials, input.stream, input.clientHeaders, input.model);
applyConfiguredUserAgent(headers, credentials.providerSpecificData);
mergeUpstreamExtraHeaders(headers, input.upstreamExtraHeaders);
@@ -446,7 +474,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 +508,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/open-sse/executors/grok-cli.ts b/open-sse/executors/grok-cli.ts
index 275e8ebfdd68..fc37b23ce2b1 100644
--- a/open-sse/executors/grok-cli.ts
+++ b/open-sse/executors/grok-cli.ts
@@ -12,13 +12,14 @@ import {
GROK_BUILD_DEFAULT_REASONING_EFFORT,
GROK_BUILD_REASONING_INCLUDE,
GROK_BUILD_RESPONSES_URL,
+ GROK_BUILD_SUPPORTED_REASONING_EFFORTS,
GROK_BUILD_TOKEN_URL,
} from "../config/grokBuild.ts";
import { resolvePublicCred } from "../utils/publicCreds.ts";
import { BaseExecutor, type ExecutorLog, type ProviderCredentials } from "./base.ts";
const GROK_BUILD_MAX_TOOLS = 200;
-const GROK_BUILD_SUPPORTED_REASONING_EFFORTS = new Set(["low", "medium", "high"]);
+const GROK_BUILD_REASONING_EFFORT_SET = new Set(GROK_BUILD_SUPPORTED_REASONING_EFFORTS);
const GROK_BUILD_REFRESH_MAX_ATTEMPTS = 3;
const GROK_BUILD_REFRESH_MIN_DELAY_MS = 200;
const GROK_BUILD_TERMINAL_REFRESH_ERRORS = new Set(["invalid_grant", "invalid_client"]);
@@ -33,7 +34,6 @@ const GROK_BUILD_UNSUPPORTED_PARAMS = [
"reasoning_effort",
];
-
/**
* Grok Build's cli-chat-proxy is stricter about Responses `function_call_output.output`
* than OpenAI's Responses API. Agent tool results can contain truncated / incomplete
@@ -128,7 +128,7 @@ function normalizeGrokBuildReasoning(
): Record | null {
const reasoning = asRequestRecord(value);
const hasExplicitEffort = Object.prototype.hasOwnProperty.call(reasoning, "effort");
- if (!GROK_BUILD_SUPPORTED_REASONING_EFFORTS.has(String(reasoning.effort))) {
+ if (!GROK_BUILD_REASONING_EFFORT_SET.has(String(reasoning.effort))) {
delete reasoning.effort;
}
if (model === "grok-composer-2.5-fast") {
diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts
index 3fc2bdf9b345..34f3bafe73c3 100644
--- a/open-sse/executors/index.ts
+++ b/open-sse/executors/index.ts
@@ -1,9 +1,6 @@
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
-import {
- registerExecutor,
- getRegisteredExecutor,
- hasRegisteredExecutor,
-} from "./registry.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";
@@ -17,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";
@@ -107,6 +105,8 @@ const executors = {
"glm-cn": new GlmExecutor("glm-cn"),
glmt: new GlmExecutor("glmt"),
cu: new CursorExecutor(), // Alias for cursor
+ "cursor-api": new CursorExecutor("cursor-api"),
+ cua: new CursorExecutor("cursor-api"),
"azure-openai": new AzureOpenAIExecutor(),
"azure-ai": new AzureAiExecutor(),
"command-code": new CommandCodeExecutor(),
@@ -118,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
@@ -235,7 +237,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/executors/opencode.ts b/open-sse/executors/opencode.ts
index 6f7a08fbaf46..51419ead9c59 100644
--- a/open-sse/executors/opencode.ts
+++ b/open-sse/executors/opencode.ts
@@ -71,7 +71,8 @@ const OPENCODE_FREE_MODELS = new Set([
* `opencode models opencode-go --verbose`; MiniMax M3 excluded — different
* thinking-mode mapping):
* grok-4.5 low/medium/high; hy3 none/low/high; kimi-k3 max;
- * qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max
+ * qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max;
+ * muse-spark-1.2-contributor minimal/low/medium/high/xhigh (no max)
*/
const EFFORT_TIERS: Record = {
"deepseek-v4-pro": EFFORT_LEVELS,
@@ -84,6 +85,7 @@ const EFFORT_TIERS: Record = {
"qwen3.6-plus": ["high", "max"],
"qwen3.7-max": ["high", "max"],
"qwen3.7-plus": ["high", "max"],
+ "muse-spark-1.2-contributor": ["minimal", "low", "medium", "high", "xhigh"],
};
/**
diff --git a/open-sse/executors/perplexity-web/protocol.ts b/open-sse/executors/perplexity-web/protocol.ts
index f98680c4ff28..12e98ccdc401 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/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts
index 32ef00311028..9dc499a1e47e 100644
--- a/open-sse/handlers/audioSpeech.ts
+++ b/open-sse/handlers/audioSpeech.ts
@@ -229,6 +229,16 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
return audioStreamResponse(res);
}
+/**
+ * Voice-note clients send response_format=ogg. OpenAI TTS documents opus, not ogg.
+ * OmniRoute already returns Ogg/Opus bytes for opus — alias ogg → opus (#10587).
+ */
+export function normalizeSpeechResponseFormat(fmt) {
+ if (typeof fmt !== "string" || !fmt) return "mp3";
+ const lower = fmt.toLowerCase();
+ return lower === "ogg" ? "opus" : lower;
+}
+
/**
* Handle Soniox TTS (OpenAI speech shape → Soniox /tts, returns raw audio bytes)
*/
@@ -963,7 +973,7 @@ export async function handleAudioSpeech({
model: modelId,
input: body.input,
voice: body.voice || "alloy",
- response_format: body.response_format || "mp3",
+ response_format: normalizeSpeechResponseFormat(body.response_format),
speed: body.speed || 1.0,
}),
});
diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts
index d54689472c0a..beb842bf2d16 100644
--- a/open-sse/handlers/chatCore.ts
+++ b/open-sse/handlers/chatCore.ts
@@ -33,7 +33,39 @@ 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,
+ 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,
@@ -41,7 +73,11 @@ import {
isStripReasoningRequested,
} from "./chatCore/headers.ts";
import { markCodexScopeRateLimited } from "./chatCore/codexFailover.ts";
-import { isCodexOriginatedHeaders } from "../config/codexIdentity.ts";
+import { getCodexClientSessionId, isCodexOriginatedHeaders } from "../config/codexIdentity.ts";
+import {
+ noteCodexTurnStateProvenance,
+ readCodexTurnStateHeader,
+} from "../config/codexTurnState.ts";
import { trackDevice, extractIpFromHeaders } from "../services/deviceTracker.ts";
import { getCombosCached } from "./chatCore/comboContextCache.ts";
export { clearCombosCache, clearUpstreamProxyConfigCache } from "./chatCore/comboContextCache.ts";
@@ -166,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,
@@ -330,7 +369,7 @@ import {
import { cacheReasoningFromAssistantMessage } from "../services/reasoningCache.ts";
import { sanitizeOpenAITool } from "../services/toolSchemaSanitizer.ts";
import { isCompactResponsesEndpoint } from "../executors/codex.ts";
-import { buildCodexQuotaPersistence } from "./chatCore/codexQuota.ts";
+import { persistCodexChildQuotaResponse } from "../services/codexAccount/index.ts";
import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts";
import { translateNonStreamingResponse } from "./responseTranslator.ts";
import { unwrapClineNonStreamingEnvelope } from "./chatCore/clineResponseEnvelope.ts";
@@ -393,8 +432,10 @@ import {
} from "../utils/aiSdkCompat.ts";
import { generateRequestId } from "@/shared/utils/requestId";
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 {
@@ -470,6 +511,7 @@ export async function handleChatCore({
conversationId = null,
modelPinned = false,
skipResourcePressureGuard = false,
+ reasoningTransportFallback = "skip",
managedLease = null,
}) {
let { provider, model, extendedContext } = modelInfo;
@@ -652,40 +694,6 @@ export async function handleChatCore({
creds: Record | null | undefined,
transport?: string
): void => recordKeyHealthStatusFor(status, creds, log, transport);
- const persistCodexQuotaState = async (headers: Record | null, status = 0) => {
- const currentConnectionId = getCurrentConnectionId();
- if (provider !== "codex" || !currentConnectionId || !headers) return;
- try {
- const existingProviderData =
- credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object"
- ? (credentials.providerSpecificData as Record)
- : {};
- // Pure payload build extracted to chatCore/codexQuota.ts (#3501). Returns null when the
- // response carries no quota headers (nothing to persist).
- const built = buildCodexQuotaPersistence({
- headers,
- existingProviderData,
- modelForScope: model || requestedModel || "",
- status,
- });
- if (!built) return;
- if (built.exhaustionLog) {
- log?.debug?.("CODEX", built.exhaustionLog);
- }
- // Invalidate the preflight cache for this connection so the next
- // isModelAvailable check fetches fresh quota data.
- if (status === 429) {
- invalidateCodexQuotaCache(currentConnectionId);
- }
- await updateProviderConnection(currentConnectionId, {
- providerSpecificData: built.nextProviderData,
- });
- credentials.providerSpecificData = built.nextProviderData;
- } catch (err) {
- const errMessage = err instanceof Error ? err.message : String(err);
- log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
- }
- };
// ── Phase 9.2: Idempotency check ──
// Resolve the idempotency key once here and reuse it at the Phase 9.2 save site below,
// rather than re-deriving it. (#3821-review LEDGER-6)
@@ -722,12 +730,13 @@ export async function handleChatCore({
copilotCompatibleReasoning,
clientResponseFormat,
} = resolveChatCoreRequestFormat({ clientRawRequest, body, provider, userAgent });
- const nativeOpenAICompatibleResponsesPassthrough = shouldUseNativeOpenAICompatibleResponsesPassthrough({
- provider,
- sourceFormat,
- endpointPath,
- providerSpecificData: credentials?.providerSpecificData,
- });
+ const nativeOpenAICompatibleResponsesPassthrough =
+ shouldUseNativeOpenAICompatibleResponsesPassthrough({
+ provider,
+ sourceFormat,
+ endpointPath,
+ providerSpecificData: credentials?.providerSpecificData,
+ });
const responsesInputItems = Array.isArray(body?.input) ? body.input : [];
const customToolNames = collectCustomToolNamesForSourceFormat(
sourceFormat,
@@ -1185,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);
@@ -2036,13 +2064,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;
@@ -2968,7 +3001,18 @@ export async function handleChatCore({
? (extractSessionAffinityKey(body, clientRawRequest?.headers) ?? null)
: null;
- while (attempts < maxAttempts) {
+ // ── Antigravity BYOP 422 account-rotation state ─────────────────────
+ // A GCP_PROJECT_REQUIRED 422 is account-specific (that Google
+ // account lacks a GCP Project ID). Rotate to a sibling antigravity
+ // account instead of surfacing the error, so multi-account setups
+ // keep working without user action. Tracked separately from
+ // maxAttempts so non-BYOP antigravity failures never get a second
+ // shot (no double upstream calls).
+ const antigravityByopExcludedIds: string[] = [];
+ let antigravityByopRotationPending = false;
+
+ while (attempts < maxAttempts || antigravityByopRotationPending) {
+ antigravityByopRotationPending = false; // consumed per iteration
trace("pre_executor", { attempt: attempts });
updatePendingScope(pendingScope, {
stage: "sending_to_provider",
@@ -3051,6 +3095,33 @@ export async function handleChatCore({
const res = normalizeExecutorResult(rawExecutorResult);
trace("post_executor", { status: res?.response?.status });
+ if (
+ provider === "codex" &&
+ attemptConnectionId &&
+ !(await shouldIsolateProbeFailures())
+ ) {
+ try {
+ const persistedQuota = await persistCodexChildQuotaResponse({
+ connectionId: String(attemptConnectionId),
+ model: modelToCall || model || requestedModel || "",
+ headers: normalizeHeaders(res.response.headers),
+ status: res.response.status,
+ });
+ if (persistedQuota) {
+ execCreds.providerSpecificData = persistedQuota.providerSpecificData;
+ if (persistedQuota.exhaustionLog) {
+ log?.debug?.("CODEX", persistedQuota.exhaustionLog);
+ }
+ }
+ if (res.response.status === 429) {
+ invalidateCodexQuotaCache(String(attemptConnectionId));
+ }
+ } catch (err) {
+ const errMessage = err instanceof Error ? err.message : String(err);
+ log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
+ }
+ }
+
// Track Gemini RPM + RPD request counts for 429 classification
if (provider === "gemini") {
incrementRequestCount(modelToCall);
@@ -3060,7 +3131,11 @@ export async function handleChatCore({
stage: "provider_response_started",
});
- if (res.response.status === 401 && executionConnectionId) {
+ if (
+ res.response.status === 401 &&
+ executionConnectionId &&
+ !(await shouldIsolateProbeFailures())
+ ) {
recordKeyHealthStatus(401, execCreds);
}
@@ -3090,7 +3165,10 @@ export async function handleChatCore({
!managedLease &&
comboStrategy !== "context-relay" &&
res.response.status === 429 &&
- attempts < maxAttempts - 1
+ attempts < maxAttempts - 1 &&
+ // Probe-origin (test-all) 429 must not rotate accounts or persist
+ // cooldowns — routing state untouched (#9817).
+ !(await shouldIsolateProbeFailures())
) {
const failedConnectionId =
executionConnectionId || credentials?.connectionId || connectionId;
@@ -3105,29 +3183,15 @@ export async function handleChatCore({
`429 on connection ${String(failedConnectionId).slice(0, 8)} (attempt ${attempts + 1}/${maxAttempts}), rotating account`
);
- // Mark only the current Codex model scope as rate-limited.
+ // Mark only the current Codex model scope as rate-limited. A connection-wide
+ // cooldown here would let a Spark limit suppress independent Sol/Terra traffic.
if (failedConnectionId) {
await markCodexScopeRateLimited({
failedConnectionId: String(failedConnectionId),
model: modelToCall || model || requestedModel || null,
rateLimitedUntil: new Date(Date.now() + (retryAfterMs || 60_000)).toISOString(),
- credentials,
+ credentials: execCreds || credentials,
});
- // Fix B: also persist the cooldown to
- // `provider_connections.rate_limited_until`. Without this,
- // the Codex 429 cascade survives the current request (via
- // `markCodexScopeRateLimited`'s in-memory Map) but is lost
- // on process restart — the same exhausted Codex key is
- // re-picked on the very next request. Mirrors
- // `open-sse/executors/antigravity.ts:343`.
- // Best-effort: never crash the chat path on DB write failure.
- try {
- const { setConnectionRateLimitUntil } = await import("@/lib/db/providers");
- const untilMs = Date.now() + (retryAfterMs || 60_000);
- setConnectionRateLimitUntil(String(failedConnectionId), untilMs);
- } catch {
- // ignore — best effort
- }
if (!codexExcludedIds.includes(String(failedConnectionId))) {
codexExcludedIds.push(String(failedConnectionId));
}
@@ -3195,6 +3259,55 @@ export async function handleChatCore({
continue;
}
+ // ── Antigravity BYOP 422 account rotation ───────────────────────
+ // GCP_PROJECT_REQUIRED (422, code gcp_project_required) means
+ // THIS Google account must Bring Its Own GCP Project. Mark the
+ // connection excluded (rateLimitedUntil, best-effort) and rotate
+ // to a sibling antigravity account so the request succeeds
+ // without user action. When no sibling exists (or all are BYOP),
+ // fall through: the error-state block excludes the connection
+ // and the actionable 422 is surfaced.
+ if (provider === "antigravity" && res.response.status === 422) {
+ const byopBody = await res.response
+ .clone()
+ .text()
+ .catch(() => "");
+ if (byopBody.includes("gcp_project_required")) {
+ const byopFailedId =
+ executionConnectionId || credentials?.connectionId || connectionId;
+ if (byopFailedId) {
+ if (!antigravityByopExcludedIds.includes(String(byopFailedId))) {
+ antigravityByopExcludedIds.push(String(byopFailedId));
+ }
+ try {
+ const { setConnectionRateLimitUntil } = await import("@/lib/db/providers");
+ setConnectionRateLimitUntil(
+ String(byopFailedId),
+ Date.now() + COOLDOWN_MS.gcpProjectRequired
+ );
+ } catch {
+ // best-effort — never break the rotation path
+ }
+ }
+ const byopNextCreds = await getProviderCredentials(
+ "antigravity",
+ null,
+ null,
+ modelToCall || model || requestedModel || null,
+ { excludeConnectionIds: [...antigravityByopExcludedIds] }
+ ).catch(() => null);
+ if (byopNextCreds && !byopNextCreds.allRateLimited) {
+ log?.warn?.(
+ "ANTIGRAVITY_BYOP_ROTATION",
+ `BYOP 422 on connection ${String(byopFailedId).slice(0, 8)} → rotating to ${String(byopNextCreds.connectionId).slice(0, 8)}`
+ );
+ Object.assign(credentials, byopNextCreds);
+ antigravityByopRotationPending = true;
+ continue;
+ }
+ }
+ }
+
// For streaming: release the semaphore when the client drains or cancels the stream.
if (stream) {
const originalBody = res.response.body;
@@ -3387,6 +3500,15 @@ export async function handleChatCore({
const responseHeaders = new Headers(headersObj);
stripStaleForwardingHeaders(responseHeaders);
stripNextMiddlewareControlHeaders(responseHeaders);
+ // The upstream headers (turn-state included) are about to be committed
+ // to the client — record which connection minted the blob so a later
+ // cross-account echo can be stripped (Codex failover guard).
+ if (provider === "codex" && readCodexTurnStateHeader(responseHeaders)) {
+ noteCodexTurnStateProvenance(
+ getCodexClientSessionId(clientRawRequest?.headers),
+ rawResult._executionCredentials?.connectionId ?? credentials?.connectionId
+ );
+ }
const contentType = (responseHeaders.get("content-type") || "").toLowerCase();
const payload = await readNonStreamingResponseBody(
rawResult.response,
@@ -3700,10 +3822,15 @@ export async function handleChatCore({
}
// Handle 401/403 - try token refresh using executor
+ // T-PROBE: probe-origin failures never attempt the refresh — a probe must
+ // not consume a rotating refresh token nor persist an "expired"
+ // deactivation on refresh failure (#9817). The 401/403 then flows into
+ // the normal providerFailure classification (record-only in probe mode).
if (
(providerResponse.status === HTTP_STATUS.UNAUTHORIZED ||
providerResponse.status === HTTP_STATUS.FORBIDDEN) &&
- !hadStreamOptions // Skip refresh if failure may be from stream_options removal, not auth
+ !hadStreamOptions && // Skip refresh if failure may be from stream_options removal, not auth
+ !(await shouldIsolateProbeFailures())
) {
// Fix A: wrap refreshCredentials in runWithOnPersist so the persist callback
// executes INSIDE the per-connection mutex held by getAccessToken. This makes
@@ -3855,8 +3982,6 @@ export async function handleChatCore({
}
}
- await persistCodexQuotaState(normalizeHeaders(providerResponse.headers), providerResponse.status);
-
// Check provider response - return error info for fallback handling
providerFailure: if (!providerResponse.ok) {
trackPendingRequest(model, provider, connectionId, false);
@@ -3985,17 +4110,34 @@ export async function handleChatCore({
if (errorConnectionId && errorType) {
try {
if (errorType === PROVIDER_ERROR_TYPES.FORBIDDEN) {
- await updateProviderConnection(errorConnectionId, {
- isActive: false,
- testStatus: "banned",
- lastErrorType: errorType,
- lastError: message,
- errorCode: statusCode,
- });
- console.warn(
- `[provider] Node ${errorConnectionId} banned (${statusCode}) — disabling permanently`
- );
+ // T-PROBE: a probe-origin failure (model test-all) must never
+ // remove the connection from the pool — record but stay active.
+ if (await shouldIsolateProbeFailures()) {
+ await updateProviderConnection(errorConnectionId, {
+ lastErrorType: errorType,
+ lastError: message,
+ errorCode: statusCode,
+ lastErrorAt: new Date().toISOString(),
+ });
+ console.warn(
+ `[provider] Node ${errorConnectionId} probe ${errorType} (${statusCode}) — connection stays active`
+ );
+ } else {
+ await updateProviderConnection(errorConnectionId, {
+ isActive: false,
+ testStatus: "banned",
+ lastErrorType: errorType,
+ lastError: message,
+ errorCode: statusCode,
+ });
+ console.warn(
+ `[provider] Node ${errorConnectionId} banned (${statusCode}) — disabling permanently`
+ );
+ }
} else if (errorType === PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED) {
+ // T-PROBE: probe-origin failures (test-all) never deactivate —
+ // record but stay active; Plan A (extra keys) stays first so the
+ // real path keeps its existing priority (#9817).
// Plan A: if connection has extra API keys, don't disable — only the failing key is affected.
// Single-key connections still get disabled as before.
if (
@@ -4013,6 +4155,16 @@ export async function handleChatCore({
console.warn(
`[provider] Node ${errorConnectionId} account deactivated (${statusCode}) — has extra keys, keeping connection active`
);
+ } else if (await shouldIsolateProbeFailures()) {
+ await updateProviderConnection(errorConnectionId, {
+ lastErrorType: errorType,
+ lastError: message,
+ errorCode: statusCode,
+ lastErrorAt: new Date().toISOString(),
+ });
+ console.warn(
+ `[provider] Node ${errorConnectionId} probe ${errorType} (${statusCode}) — connection stays active`
+ );
} else {
await updateProviderConnection(errorConnectionId, {
isActive: false,
@@ -4026,73 +4178,90 @@ export async function handleChatCore({
);
}
} else if (errorType === PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED) {
- // Kimi's 403 says "billing cycle" for both an exhausted subscription and a
- // temporary request window. Read its official usage endpoint before making
- // the connection terminal: a non-zero Weekly quota plus an empty Ratelimit
- // window must recover automatically at the reported reset time.
- let kimiRateLimitResetAt: string | null = null;
- if (provider === "kimi-coding") {
- try {
- const { fetchAndPersistProviderLimits } = await import("@/lib/usage/providerLimits");
- const { usage } = await fetchAndPersistProviderLimits(errorConnectionId, "manual");
- kimiRateLimitResetAt = getKimiTemporaryRateLimitResetAt(usage);
- } catch {
- // Preserve the existing quota handling when Kimi's usage endpoint is unavailable.
- }
- }
-
- // Providers with per-model quotas — lock the model only, not the connection
- const quotaCooldownMs = kimiRateLimitResetAt
- ? Math.max(new Date(kimiRateLimitResetAt).getTime() - Date.now(), 0)
- : retryAfterMs || COOLDOWN_MS.rateLimit;
- const accountSemaphoreKey = resolveAccountSemaphoreKey({
- provider,
- model: currentModel,
- connectionId: errorConnectionId,
- credentials,
- });
- if (accountSemaphoreKey) {
- markAccountSemaphoreBlocked(accountSemaphoreKey, quotaCooldownMs);
- }
- if (kimiRateLimitResetAt) {
+ // T-PROBE: probe-origin failures never write quota state —
+ // `testStatus: "credits_exhausted"` is terminal and removes the
+ // connection from the pool; semaphore locks and per-model quota
+ // lockouts are routing mutations too. Record only (#9817).
+ if (await shouldIsolateProbeFailures()) {
await updateProviderConnection(errorConnectionId, {
- testStatus: "unavailable",
- rateLimitedUntil: kimiRateLimitResetAt,
- backoffLevel: 0,
- lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED,
+ lastErrorType: errorType,
lastError: message,
errorCode: statusCode,
+ lastErrorAt: new Date().toISOString(),
});
console.warn(
- `[provider] Node ${errorConnectionId} Kimi request window exhausted (${statusCode}) — retrying after ${kimiRateLimitResetAt}`
- );
- } else if (isModelScope() && errorConnectionId) {
- const lockFn = provider === "antigravity" ? lockExactModel : lockModel;
- lockFn(provider, errorConnectionId, model, "quota_exhausted", quotaCooldownMs);
- console.warn(
- `[provider] Node ${errorConnectionId} ModelScope model quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (connection stays active)`
- );
- } else if (
- lockModelIfPerModelQuota(
- provider,
- errorConnectionId,
- model,
- "quota_exhausted",
- quotaCooldownMs
- )
- ) {
- const quotaScope = getQuotaScopeLabelForProvider(provider, model);
- console.warn(
- `[provider] Node ${errorConnectionId} ${quotaScope}-only quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (cooldown_scope=${quotaScope}, ttl_source=${retryAfterMs ? "upstream" : "inferred"}, connection stays active)`
+ `[provider] Node ${errorConnectionId} probe ${errorType} (${statusCode}) — connection stays active`
);
} else {
- await updateProviderConnection(errorConnectionId, {
- testStatus: "credits_exhausted",
- lastErrorType: errorType,
- lastError: message,
- errorCode: statusCode,
+ // Kimi's 403 says "billing cycle" for both an exhausted subscription and a
+ // temporary request window. Read its official usage endpoint before making
+ // the connection terminal: a non-zero Weekly quota plus an empty Ratelimit
+ // window must recover automatically at the reported reset time.
+ let kimiRateLimitResetAt: string | null = null;
+ if (provider === "kimi-coding") {
+ try {
+ const { fetchAndPersistProviderLimits } =
+ await import("@/lib/usage/providerLimits");
+ const { usage } = await fetchAndPersistProviderLimits(errorConnectionId, "manual");
+ kimiRateLimitResetAt = getKimiTemporaryRateLimitResetAt(usage);
+ } catch {
+ // Preserve the existing quota handling when Kimi's usage endpoint is unavailable.
+ }
+ }
+
+ // Providers with per-model quotas — lock the model only, not the connection
+ const quotaCooldownMs = kimiRateLimitResetAt
+ ? Math.max(new Date(kimiRateLimitResetAt).getTime() - Date.now(), 0)
+ : retryAfterMs || COOLDOWN_MS.rateLimit;
+ const accountSemaphoreKey = resolveAccountSemaphoreKey({
+ provider,
+ model: currentModel,
+ connectionId: errorConnectionId,
+ credentials,
});
- console.warn(`[provider] Node ${errorConnectionId} exhausted quota (${statusCode})`);
+ if (accountSemaphoreKey) {
+ markAccountSemaphoreBlocked(accountSemaphoreKey, quotaCooldownMs);
+ }
+ if (kimiRateLimitResetAt) {
+ await updateProviderConnection(errorConnectionId, {
+ testStatus: "unavailable",
+ rateLimitedUntil: kimiRateLimitResetAt,
+ backoffLevel: 0,
+ lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED,
+ lastError: message,
+ errorCode: statusCode,
+ });
+ console.warn(
+ `[provider] Node ${errorConnectionId} Kimi request window exhausted (${statusCode}) — retrying after ${kimiRateLimitResetAt}`
+ );
+ } else if (isModelScope() && errorConnectionId) {
+ const lockFn = provider === "antigravity" ? lockExactModel : lockModel;
+ lockFn(provider, errorConnectionId, model, "quota_exhausted", quotaCooldownMs);
+ console.warn(
+ `[provider] Node ${errorConnectionId} ModelScope model quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (connection stays active)`
+ );
+ } else if (
+ lockModelIfPerModelQuota(
+ provider,
+ errorConnectionId,
+ model,
+ "quota_exhausted",
+ quotaCooldownMs
+ )
+ ) {
+ const quotaScope = getQuotaScopeLabelForProvider(provider, model);
+ console.warn(
+ `[provider] Node ${errorConnectionId} ${quotaScope}-only quota exhausted (${statusCode}) for ${model} - ${Math.ceil(quotaCooldownMs / 1000)}s (cooldown_scope=${quotaScope}, ttl_source=${retryAfterMs ? "upstream" : "inferred"}, connection stays active)`
+ );
+ } else {
+ await updateProviderConnection(errorConnectionId, {
+ testStatus: "credits_exhausted",
+ lastErrorType: errorType,
+ lastError: message,
+ errorCode: statusCode,
+ });
+ console.warn(`[provider] Node ${errorConnectionId} exhausted quota (${statusCode})`);
+ }
}
} else if (errorType === PROVIDER_ERROR_TYPES.UNAUTHORIZED) {
// Normal 401 (token/session auth issue): keep account active for refresh/re-auth.
@@ -4134,14 +4303,39 @@ export async function handleChatCore({
lastError: message,
errorCode: statusCode,
});
+ // T-PROBE: the 24h exclusion is a routing mutation — a probe must
+ // not push a connection into a day-long cooldown (#9817).
+ if (!(await shouldIsolateProbeFailures())) {
+ try {
+ const { setConnectionRateLimitUntil } = await import("@/lib/db/providers");
+ setConnectionRateLimitUntil(errorConnectionId, Date.now() + geoCooldownMs);
+ } catch {
+ // DB write failure must never break the fallback loop
+ }
+ }
+ console.warn(
+ `[provider] Node ${errorConnectionId} geo-blocked (${statusCode}) — excluded for ${Math.ceil(geoCooldownMs / 1000)}s, trying other accounts`
+ );
+ } else if (errorType === PROVIDER_ERROR_TYPES.GCP_PROJECT_REQUIRED) {
+ // Antigravity BYOP: the account must Bring Its Own GCP Project.
+ // Account-specific and fixable by entering a Project ID — never a
+ // model lockout, never a ban. Exclude the connection for the
+ // cooldown window so selection prefers sibling accounts; the 422
+ // body carries the actionable message when no sibling is available.
+ const byopCooldownMs = COOLDOWN_MS.gcpProjectRequired ?? 24 * 60 * 60 * 1000;
+ await updateProviderConnection(errorConnectionId, {
+ lastErrorType: errorType,
+ lastError: message,
+ errorCode: statusCode,
+ });
try {
const { setConnectionRateLimitUntil } = await import("@/lib/db/providers");
- setConnectionRateLimitUntil(errorConnectionId, Date.now() + geoCooldownMs);
+ setConnectionRateLimitUntil(errorConnectionId, Date.now() + byopCooldownMs);
} catch {
- // DB write failure must never break the fallback loop
+ // best-effort — never break the error path
}
console.warn(
- `[provider] Node ${errorConnectionId} geo-blocked (${statusCode}) — excluded for ${Math.ceil(geoCooldownMs / 1000)}s, trying other accounts`
+ `[provider] Node ${errorConnectionId} GCP project required (${statusCode}) — excluded for ${Math.ceil(byopCooldownMs / 1000)}s, routing to other accounts (enter a Project ID to restore)`
);
} else if (errorType === PROVIDER_ERROR_TYPES.MODEL_NOT_FOUND) {
// 404 — model/endpoint does not exist upstream. Lock the model so the
@@ -4149,16 +4343,20 @@ export async function handleChatCore({
// otherwise degenerate into a 429 rate-limit storm). Connection stays
// active since only the specific model is unavailable. (#6827)
const notFoundCooldownMs = COOLDOWN_MS.notFound;
- lockModel(
- provider,
- errorConnectionId,
- currentModel,
- "model_not_found",
- notFoundCooldownMs
- );
- console.warn(
- `[provider] Node ${errorConnectionId} model not found (${statusCode}) for ${currentModel} - locking model for ${Math.ceil(notFoundCooldownMs / 1000)}s (connection stays active)`
- );
+ // T-PROBE: the model lockout is a routing mutation — a probe must
+ // not lock a model for the cooldown window (#9817).
+ if (!(await shouldIsolateProbeFailures())) {
+ lockModel(
+ provider,
+ errorConnectionId,
+ currentModel,
+ "model_not_found",
+ notFoundCooldownMs
+ );
+ console.warn(
+ `[provider] Node ${errorConnectionId} model not found (${statusCode}) for ${currentModel} - locking model for ${Math.ceil(notFoundCooldownMs / 1000)}s (connection stays active)`
+ );
+ }
}
} catch {
// Best-effort state update; request flow should continue with fallback handling.
@@ -4783,9 +4981,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;
@@ -4917,6 +5117,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,
@@ -5017,6 +5238,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),
@@ -5106,6 +5364,17 @@ export async function handleChatCore({
comboStrategy,
});
+ // The streaming headers (turn-state included, when present) are committed to
+ // the client from here on — record which connection minted the blob so a
+ // later cross-account echo can be stripped (Codex failover guard). The
+ // in-place failover update means `credentials` is the winning account.
+ if (provider === "codex" && readCodexTurnStateHeader(providerResponse.headers)) {
+ noteCodexTurnStateProvenance(
+ getCodexClientSessionId(clientRawRequest?.headers),
+ credentials?.connectionId
+ );
+ }
+
// Create transform stream with logger for streaming response
let transformStream;
const responseToolNameMap = mergeResponseToolNameMap(
@@ -5126,6 +5395,8 @@ export async function handleChatCore({
error: streamError,
errorCode: streamErrorCode,
ttft,
+ itlMs: streamItlMs,
+ interrupted: streamInterrupted,
}) => {
const normalizedStreamStatus = streamStatus || 200;
if (streamCompletionRecorded) return;
@@ -5229,6 +5500,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/handlers/chatCore/codexFailover.ts b/open-sse/handlers/chatCore/codexFailover.ts
index f552af737537..d48cb7d4bca1 100644
--- a/open-sse/handlers/chatCore/codexFailover.ts
+++ b/open-sse/handlers/chatCore/codexFailover.ts
@@ -1,42 +1,29 @@
-import { getCodexModelScope } from "../../config/codexQuotaScopes.ts";
-import { updateProviderConnection } from "@/lib/db/providers";
-import { getCachedProviderConnectionById } from "@/lib/localDb";
+import { persistCodexChildCooldown } from "../../services/codexAccount/index.ts";
type CodexFailoverCredentials = {
connectionId?: string | null;
providerSpecificData?: unknown;
};
-function asProviderData(value: unknown): Record {
- return value && typeof value === "object" ? (value as Record) : {};
-}
-
export async function markCodexScopeRateLimited(params: {
failedConnectionId: string;
model: string | null;
rateLimitedUntil: string;
credentials?: CodexFailoverCredentials | null;
}): Promise {
- const connection = await getCachedProviderConnectionById(params.failedConnectionId).catch(() => null);
- const existingProviderData = connection
- ? asProviderData(connection.providerSpecificData)
- : asProviderData(params.credentials?.providerSpecificData);
- const existingScopeMap = asProviderData(existingProviderData.codexScopeRateLimitedUntil);
- const nextProviderData = {
- ...existingProviderData,
- codexScopeRateLimitedUntil: {
- ...existingScopeMap,
- [getCodexModelScope(params.model || "")]: params.rateLimitedUntil,
- },
- };
-
- updateProviderConnection(params.failedConnectionId, {
- ...(connection ? { providerSpecificData: nextProviderData } : {}),
- lastError: "429 rate limited — codex account rotation",
- errorCode: 429,
- }).catch(() => {});
+ const persisted = params.model
+ ? await persistCodexChildCooldown({
+ connectionId: params.failedConnectionId,
+ model: params.model,
+ rateLimitedUntil: params.rateLimitedUntil,
+ }).catch(() => null)
+ : null;
- if (params.credentials && String(params.credentials.connectionId) === params.failedConnectionId) {
- params.credentials.providerSpecificData = nextProviderData;
+ if (
+ persisted &&
+ params.credentials &&
+ String(params.credentials.connectionId) === params.failedConnectionId
+ ) {
+ params.credentials.providerSpecificData = persisted.providerSpecificData;
}
}
diff --git a/open-sse/handlers/chatCore/codexQuota.ts b/open-sse/handlers/chatCore/codexQuota.ts
deleted file mode 100644
index 7bc6eac85199..000000000000
--- a/open-sse/handlers/chatCore/codexQuota.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * chatCore Codex quota-persistence builder (Quality Gate v2 / Fase 9 — chatCore god-file
- * decomposition, #3501).
- *
- * Pure core of handleChatCore's persistCodexQuotaState: turns the upstream Codex quota response
- * headers into the next `providerSpecificData` payload (the codexQuotaState snapshot, plus — on a
- * 429 whose dual-window usage is past the exhaustion threshold — the per-scope cooldown timestamp,
- * the exhausted window, and the debug-log message). The handler keeps the impure parts byte-
- * identically: the DB write (updateProviderConnection), the preflight-cache invalidation on every
- * 429, the credentials mutation, and emitting the returned log line.
- */
-
-import {
- parseCodexQuotaHeaders,
- getCodexModelScope,
- getCodexDualWindowCooldownMs,
-} from "../../executors/codex.ts";
-
-export type CodexQuotaPersistence = {
- /** The merged providerSpecificData to persist (existing data + codexQuotaState [+ 429 cooldown]). */
- nextProviderData: Record;
- /** The CODEX debug-log message to emit when a 429 exhausted a window, else null. */
- exhaustionLog: string | null;
-};
-
-/**
- * Build the providerSpecificData update for a Codex quota response. Returns null when the response
- * carries no quota headers (nothing to persist). Pure: a function of the headers, the existing
- * provider data, the model used for scope resolution, and the upstream status.
- */
-export function buildCodexQuotaPersistence(opts: {
- headers: Record;
- existingProviderData: Record;
- modelForScope: string;
- status: number;
-}): CodexQuotaPersistence | null {
- const { headers, existingProviderData, modelForScope, status } = opts;
-
- const quota = parseCodexQuotaHeaders(headers);
- if (!quota) return null;
-
- const scope = getCodexModelScope(modelForScope);
- const quotaState = {
- usage5h: quota.usage5h,
- limit5h: quota.limit5h,
- resetAt5h: quota.resetAt5h,
- usage7d: quota.usage7d,
- limit7d: quota.limit7d,
- resetAt7d: quota.resetAt7d,
- scope,
- updatedAt: new Date().toISOString(),
- };
-
- const nextProviderData: Record = {
- ...existingProviderData,
- codexQuotaState: quotaState,
- };
-
- let exhaustionLog: string | null = null;
-
- // T03/T09: on 429, persist exact reset time per scope to avoid global over-blocking.
- // Use dual-window cooldown to distinguish short-term and weekly Codex exhaustion.
- if (status === 429) {
- const { cooldownMs, window: exhaustedWindow } = getCodexDualWindowCooldownMs(quota);
- if (cooldownMs > 0) {
- const scopeUntil = new Date(Date.now() + cooldownMs).toISOString();
- const scopeMapRaw =
- existingProviderData &&
- typeof existingProviderData === "object" &&
- existingProviderData.codexScopeRateLimitedUntil &&
- typeof existingProviderData.codexScopeRateLimitedUntil === "object"
- ? existingProviderData.codexScopeRateLimitedUntil
- : {};
-
- nextProviderData.codexScopeRateLimitedUntil = {
- ...(scopeMapRaw as Record),
- [scope]: scopeUntil,
- };
- nextProviderData.codexExhaustedWindow = exhaustedWindow;
- exhaustionLog = `Quota exhaustion on ${exhaustedWindow} window, cooldown until ${scopeUntil}`;
- }
- }
-
- return { nextProviderData, exhaustionLog };
-}
diff --git a/open-sse/handlers/chatCore/memorySkillsInjection.ts b/open-sse/handlers/chatCore/memorySkillsInjection.ts
index 2a43c17cbf75..14fbdc957539 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/handlers/chatCore/responseHeaders.ts b/open-sse/handlers/chatCore/responseHeaders.ts
index 59c45ba8291c..8206304544fc 100644
--- a/open-sse/handlers/chatCore/responseHeaders.ts
+++ b/open-sse/handlers/chatCore/responseHeaders.ts
@@ -28,11 +28,18 @@ const STREAMING_RESPONSE_HEADER_DENYLIST = new Set([
"x-amz-security-token",
"x-auth-token",
"x-accel-buffering",
- // 314-byte Codex session blob. It is not a client rate-limit signal and
- // alone ate ~40% of the old 768-byte budget, evicting x-codex-*-used-percent.
- "x-codex-turn-state",
]);
+/**
+ * `x-codex-turn-state` is forwarded verbatim and EXEMPT from the forwarding
+ * budget. The real Codex client captures this ~314-byte blob from /responses
+ * (and echoes it back within the same turn), so dropping it breaks the
+ * protocol chain — but naively counting it against the budget used to evict
+ * the x-codex-*-used-percent quota headers (the reason it was denylisted
+ * under #10315-era budgeting). Carving it out keeps both.
+ */
+const CODEX_TURN_STATE_RESPONSE_HEADER = "x-codex-turn-state";
+
const DEFAULT_FORWARDED_HEADER_BUDGET_BYTES = 768;
/**
@@ -206,7 +213,9 @@ export function buildStreamingResponseHeaders(
STREAMING_RESPONSE_HEADER_DENYLIST.has(normalized) ||
connectionScopedHeaders.has(normalized) ||
isNextMiddlewareControlHeader(normalized) ||
- isOmniRouteInternalHeader(normalized)
+ isOmniRouteInternalHeader(normalized) ||
+ // Forwarded separately below, outside the byte budget.
+ normalized === CODEX_TURN_STATE_RESPONSE_HEADER
) {
return;
}
@@ -269,6 +278,10 @@ export function buildStreamingResponseHeaders(
"X-Accel-Buffering": "no",
[OMNIROUTE_RESPONSE_HEADERS.cache]: "MISS",
};
+ const codexTurnState = providerHeaders.get(CODEX_TURN_STATE_RESPONSE_HEADER)?.trim();
+ if (codexTurnState) {
+ responseHeaders[CODEX_TURN_STATE_RESPONSE_HEADER] = codexTurnState;
+ }
attachOmniRouteMetaHeaders(responseHeaders, meta);
return responseHeaders;
}
diff --git a/open-sse/handlers/cursorCliProxy.ts b/open-sse/handlers/cursorCliProxy.ts
new file mode 100644
index 000000000000..dbf742f8f674
--- /dev/null
+++ b/open-sse/handlers/cursorCliProxy.ts
@@ -0,0 +1,524 @@
+/**
+ * Cursor CLI passthrough.
+ *
+ * cursor-agent honours `--endpoint` / CURSOR_API_ENDPOINT and, with
+ * `network.useHttp1ForAgent: true`, talks to that endpoint exclusively over
+ * HTTP/1.1: unary Connect-RPC POSTs (`/aiserver.v1.*`, `/agent.v1.*`,
+ * `/aiserver.v1.BidiService/BidiAppend`), the agent turn as
+ * `/agent.v1.AgentService/RunSSE` (text/event-stream), OTLP traces on
+ * `/v1/traces`, and the API-key bootstrap `POST /auth/exchange_user_api_key`.
+ *
+ * Pointing the CLI at OmniRoute therefore only needs a thin forwarder:
+ * 1. `/auth/exchange_user_api_key` authenticates the CLI with an OmniRoute
+ * API key and hands back an OmniRoute-minted session JWT. The CLI reads
+ * `exp` from whatever JWT it receives and re-exchanges when the token is
+ * opaque or expired, so the minted token must be a real JWT with `exp`.
+ * 2. Every other path verifies that JWT, resolves an active `cursor-api`
+ * connection (the crsr_ key is exchanged for a session token), swaps the
+ * Authorization header and streams the upstream reply back unchanged.
+ * Each hop is recorded in call_logs.
+ */
+
+import { SignJWT, jwtVerify, type JWTPayload } from "jose";
+import { z } from "zod";
+import { getApiKeyById, getApiKeyMetadata, validateApiKey } from "@/lib/db/apiKeys";
+import { getProviderConnections } from "@/lib/db/providers";
+import { saveCallLog } from "@/lib/usage/callLogs";
+import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags";
+import { HTTP_STATUS } from "../config/constants.ts";
+import {
+ CURSOR_API_BASE_URL,
+ CURSOR_API_KEY_EXCHANGE_PATH,
+ CursorApiKeyExchangeError,
+ invalidateCursorSessionToken,
+ isCursorApiKey,
+ resolveCursorBearerToken,
+} from "../services/cursorApiKeyAuth.ts";
+import { sanitizeErrorMessage } from "../utils/error.ts";
+
+export const CURSOR_CLI_PROXY_PREFIX = "/api/cursor-cli";
+export const CURSOR_CLI_SESSION_ISSUER = "omniroute";
+export const CURSOR_CLI_SESSION_AUDIENCE = "cursor-cli";
+export const CURSOR_CLI_SESSION_TTL_SECONDS = 60 * 60;
+export const CURSOR_CLI_REQUEST_TYPE = "cursor-cli";
+const ANONYMOUS_SUBJECT = "anonymous";
+const PROVIDER_ID = "cursor-api";
+
+const REQUEST_HEADER_DENYLIST = new Set([
+ "authorization",
+ "host",
+ "connection",
+ "content-length",
+ "accept-encoding",
+ "keep-alive",
+ "proxy-authorization",
+ "proxy-connection",
+ "te",
+ "trailer",
+ "transfer-encoding",
+ "upgrade",
+ "x-forwarded-for",
+ "x-forwarded-host",
+ "x-forwarded-proto",
+ "x-real-ip",
+ "cookie",
+]);
+
+const RESPONSE_HEADER_DENYLIST = new Set([
+ "connection",
+ "content-encoding",
+ "content-length",
+ "keep-alive",
+ "transfer-encoding",
+ "set-cookie",
+]);
+
+const exchangeBodySchema = z.object({}).passthrough();
+
+const sessionClaimsSchema = z.object({
+ sub: z.string().min(1),
+ iss: z.literal(CURSOR_CLI_SESSION_ISSUER),
+ aud: z.union([
+ z.literal(CURSOR_CLI_SESSION_AUDIENCE),
+ z.array(z.string()).refine((list) => list.includes(CURSOR_CLI_SESSION_AUDIENCE)),
+ ]),
+ exp: z.number(),
+ name: z.string().nullable().optional(),
+});
+
+export type CursorCliPrincipal = {
+ apiKeyId: string | null;
+ apiKeyName: string | null;
+};
+
+export type CursorCliConnectionLike = {
+ id?: unknown;
+ apiKey?: unknown;
+ accessToken?: unknown;
+ priority?: unknown;
+ rateLimitedUntil?: unknown;
+};
+
+export type CursorCliProxyDeps = {
+ fetchImpl: typeof fetch;
+ now: () => number;
+ getSecret: () => string | undefined;
+ validateApiKey: (key: string) => Promise;
+ getApiKeyMetadata: (key: string) => Promise<{ id: string; name: string } | null>;
+ getApiKeyById: (id: string) => Promise<{ isActive?: unknown; revokedAt?: unknown } | null>;
+ requireApiKey: () => boolean;
+ listCursorConnections: () => Promise;
+ resolveBearer: (credentials: {
+ apiKey?: string | null;
+ accessToken?: string | null;
+ }) => Promise;
+ invalidateBearer: (apiKey: string) => void;
+ saveCallLog: (entry: Record) => Promise;
+ upstreamBaseUrl: string;
+};
+
+const defaultDeps: CursorCliProxyDeps = {
+ fetchImpl: (input, init) => fetch(input, init),
+ now: () => Date.now(),
+ getSecret: () => process.env.JWT_SECRET,
+ validateApiKey: (key) => validateApiKey(key),
+ getApiKeyMetadata: async (key) => {
+ const meta = await getApiKeyMetadata(key);
+ return meta ? { id: meta.id, name: meta.name } : null;
+ },
+ getApiKeyById: (id) => getApiKeyById(id),
+ requireApiKey: () => isRequireApiKeyEnabled(),
+ listCursorConnections: async () =>
+ (await getProviderConnections({
+ provider: PROVIDER_ID,
+ isActive: true,
+ })) as CursorCliConnectionLike[],
+ resolveBearer: (credentials) => resolveCursorBearerToken(credentials),
+ invalidateBearer: (apiKey) => invalidateCursorSessionToken(apiKey),
+ saveCallLog: (entry) => saveCallLog(entry),
+ upstreamBaseUrl: CURSOR_API_BASE_URL,
+};
+
+function jsonResponse(status: number, body: Record): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { "content-type": "application/json" },
+ });
+}
+
+function connectError(status: number, code: string, message: string): Response {
+ return jsonResponse(status, { code, message: sanitizeErrorMessage(message) });
+}
+
+function extractBearer(request: Request): string | null {
+ const header = request.headers.get("authorization") ?? "";
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
+ return match ? match[1].trim() : null;
+}
+
+function secretKey(secret: string): Uint8Array {
+ return new TextEncoder().encode(secret);
+}
+
+export function normalizeCursorCliPath(segments: readonly string[]): string {
+ return "/" + segments.map((segment) => encodeURIComponent(decodeURIComponent(segment))).join("/");
+}
+
+async function authenticateExchange(
+ request: Request,
+ deps: CursorCliProxyDeps
+): Promise {
+ const bearer = extractBearer(request);
+ if (bearer && (await deps.validateApiKey(bearer))) {
+ const meta = await deps.getApiKeyMetadata(bearer);
+ return { apiKeyId: meta?.id ?? null, apiKeyName: meta?.name ?? null };
+ }
+ if (!deps.requireApiKey()) {
+ return { apiKeyId: null, apiKeyName: null };
+ }
+ return connectError(
+ HTTP_STATUS.UNAUTHORIZED,
+ "unauthenticated",
+ "CURSOR_API_KEY must be an OmniRoute API key when OmniRoute requires API keys"
+ );
+}
+
+export async function mintCursorCliSessionToken(
+ principal: CursorCliPrincipal,
+ secret: string,
+ nowMs: number
+): Promise {
+ const nowSeconds = Math.floor(nowMs / 1000);
+ return new SignJWT({ name: principal.apiKeyName })
+ .setProtectedHeader({ alg: "HS256", typ: "JWT" })
+ .setIssuer(CURSOR_CLI_SESSION_ISSUER)
+ .setAudience(CURSOR_CLI_SESSION_AUDIENCE)
+ .setSubject(principal.apiKeyId ?? ANONYMOUS_SUBJECT)
+ .setIssuedAt(nowSeconds)
+ .setExpirationTime(nowSeconds + CURSOR_CLI_SESSION_TTL_SECONDS)
+ .sign(secretKey(secret));
+}
+
+async function verifyCursorCliSessionToken(
+ token: string,
+ secret: string,
+ nowMs: number
+): Promise {
+ let payload: JWTPayload;
+ try {
+ ({ payload } = await jwtVerify(token, secretKey(secret), {
+ issuer: CURSOR_CLI_SESSION_ISSUER,
+ audience: CURSOR_CLI_SESSION_AUDIENCE,
+ currentDate: new Date(nowMs),
+ }));
+ } catch {
+ return null;
+ }
+ const claims = sessionClaimsSchema.safeParse(payload);
+ if (!claims.success) return null;
+ return {
+ apiKeyId: claims.data.sub === ANONYMOUS_SUBJECT ? null : claims.data.sub,
+ apiKeyName: claims.data.name ?? null,
+ };
+}
+
+async function isPrincipalStillValid(
+ principal: CursorCliPrincipal,
+ deps: CursorCliProxyDeps
+): Promise {
+ if (!principal.apiKeyId) return !deps.requireApiKey();
+ const row = await deps.getApiKeyById(principal.apiKeyId);
+ if (!row) return false;
+ if (row.isActive === false) return false;
+ return !(typeof row.revokedAt === "string" && row.revokedAt.trim() !== "");
+}
+
+type ResolvedConnection = {
+ connectionId: string | null;
+ bearer: string;
+ apiKey: string | null;
+};
+
+function connectionPriority(connection: CursorCliConnectionLike): number {
+ return typeof connection.priority === "number" ? connection.priority : Number.MAX_SAFE_INTEGER;
+}
+
+function isCoolingDown(connection: CursorCliConnectionLike, nowMs: number): boolean {
+ if (typeof connection.rateLimitedUntil !== "string") return false;
+ const until = Date.parse(connection.rateLimitedUntil);
+ return Number.isFinite(until) && until > nowMs;
+}
+
+async function resolveUpstreamConnection(
+ deps: CursorCliProxyDeps
+): Promise {
+ const connections = (await deps.listCursorConnections())
+ .filter((connection) => !isCoolingDown(connection, deps.now()))
+ .sort((a, b) => connectionPriority(a) - connectionPriority(b));
+ if (connections.length === 0) {
+ return connectError(
+ HTTP_STATUS.SERVICE_UNAVAILABLE,
+ "unavailable",
+ "No active Cursor API connection configured in OmniRoute"
+ );
+ }
+ let lastError: unknown = null;
+ for (const connection of connections) {
+ const apiKey = isCursorApiKey(connection.apiKey) ? connection.apiKey : null;
+ const accessToken = typeof connection.accessToken === "string" ? connection.accessToken : null;
+ try {
+ const bearer = await deps.resolveBearer({ apiKey, accessToken });
+ return {
+ connectionId: typeof connection.id === "string" ? connection.id : null,
+ bearer,
+ apiKey,
+ };
+ } catch (err) {
+ lastError = err;
+ }
+ }
+ const status =
+ lastError instanceof CursorApiKeyExchangeError ? lastError.status : HTTP_STATUS.BAD_GATEWAY;
+ const message = lastError instanceof Error ? lastError.message : "Cursor credential unavailable";
+ return connectError(
+ status,
+ status === HTTP_STATUS.UNAUTHORIZED ? "unauthenticated" : "unavailable",
+ message
+ );
+}
+
+function buildUpstreamHeaders(request: Request, bearer: string): Headers {
+ const headers = new Headers();
+ request.headers.forEach((value, name) => {
+ if (!REQUEST_HEADER_DENYLIST.has(name.toLowerCase())) headers.set(name, value);
+ });
+ headers.set("authorization", `Bearer ${bearer}`);
+ return headers;
+}
+
+function buildDownstreamHeaders(upstream: Response): Headers {
+ const headers = new Headers();
+ upstream.headers.forEach((value, name) => {
+ if (!RESPONSE_HEADER_DENYLIST.has(name.toLowerCase())) headers.set(name, value);
+ });
+ return headers;
+}
+
+type CallLogInput = {
+ method: string;
+ path: string;
+ status: number;
+ startedAt: number;
+ principal: CursorCliPrincipal | null;
+ connectionId: string | null;
+ error?: string | null;
+};
+
+function recordCall(deps: CursorCliProxyDeps, input: CallLogInput): void {
+ void deps
+ .saveCallLog({
+ method: input.method,
+ path: `${CURSOR_CLI_PROXY_PREFIX}${input.path}`,
+ status: input.status,
+ model: "-",
+ provider: PROVIDER_ID,
+ connectionId: input.connectionId,
+ duration: Math.max(0, deps.now() - input.startedAt),
+ apiKeyId: input.principal?.apiKeyId ?? null,
+ apiKeyName: input.principal?.apiKeyName ?? null,
+ requestType: CURSOR_CLI_REQUEST_TYPE,
+ sourceFormat: CURSOR_CLI_REQUEST_TYPE,
+ targetFormat: CURSOR_CLI_REQUEST_TYPE,
+ error: input.error ? { message: sanitizeErrorMessage(input.error) } : null,
+ })
+ .catch(() => undefined);
+}
+
+function streamWithCompletionLog(
+ body: ReadableStream,
+ onDone: (error?: string) => void
+): ReadableStream {
+ const reader = body.getReader();
+ let settled = false;
+ const settle = (error?: string) => {
+ if (settled) return;
+ settled = true;
+ onDone(error);
+ };
+ return new ReadableStream({
+ async pull(controller) {
+ try {
+ const { done, value } = await reader.read();
+ if (done) {
+ settle();
+ controller.close();
+ return;
+ }
+ controller.enqueue(value);
+ } catch (err) {
+ settle(err instanceof Error ? err.message : "upstream stream failed");
+ controller.error(err);
+ }
+ },
+ cancel(reason) {
+ settle(reason instanceof Error ? reason.message : "stream cancelled");
+ return reader.cancel(reason);
+ },
+ });
+}
+
+async function handleExchange(
+ request: Request,
+ startedAt: number,
+ deps: CursorCliProxyDeps
+): Promise {
+ if (request.method !== "POST") {
+ return connectError(405, "unimplemented", "Use POST");
+ }
+ const rawBody = await request.text();
+ if (rawBody.trim().length > 0) {
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(rawBody);
+ } catch {
+ return connectError(HTTP_STATUS.BAD_REQUEST, "invalid_argument", "Body must be JSON");
+ }
+ if (!exchangeBodySchema.safeParse(parsed).success) {
+ return connectError(
+ HTTP_STATUS.BAD_REQUEST,
+ "invalid_argument",
+ "Body must be a JSON object"
+ );
+ }
+ }
+
+ const principal = await authenticateExchange(request, deps);
+ if (principal instanceof Response) {
+ recordCall(deps, {
+ method: request.method,
+ path: CURSOR_API_KEY_EXCHANGE_PATH,
+ status: principal.status,
+ startedAt,
+ principal: null,
+ connectionId: null,
+ error: "OmniRoute API key rejected",
+ });
+ return principal;
+ }
+
+ const secret = deps.getSecret();
+ if (!secret || secret.trim().length === 0) {
+ return connectError(
+ HTTP_STATUS.SERVICE_UNAVAILABLE,
+ "unavailable",
+ "JWT_SECRET is not configured; the Cursor CLI passthrough cannot mint session tokens"
+ );
+ }
+
+ const token = await mintCursorCliSessionToken(principal, secret, deps.now());
+ recordCall(deps, {
+ method: request.method,
+ path: CURSOR_API_KEY_EXCHANGE_PATH,
+ status: 200,
+ startedAt,
+ principal,
+ connectionId: null,
+ });
+ return jsonResponse(200, { accessToken: token, refreshToken: token });
+}
+
+async function handleForward(
+ request: Request,
+ path: string,
+ startedAt: number,
+ deps: CursorCliProxyDeps
+): Promise {
+ const secret = deps.getSecret();
+ const bearer = extractBearer(request);
+ const principal =
+ bearer && secret ? await verifyCursorCliSessionToken(bearer, secret, deps.now()) : null;
+ if (!principal || !(await isPrincipalStillValid(principal, deps))) {
+ return connectError(
+ HTTP_STATUS.UNAUTHORIZED,
+ "unauthenticated",
+ "Missing or expired OmniRoute Cursor CLI session token"
+ );
+ }
+
+ const resolved = await resolveUpstreamConnection(deps);
+ if (resolved instanceof Response) {
+ recordCall(deps, {
+ method: request.method,
+ path,
+ status: resolved.status,
+ startedAt,
+ principal,
+ connectionId: null,
+ error: "No usable Cursor connection",
+ });
+ return resolved;
+ }
+
+ const search = new URL(request.url).search;
+ const upstreamUrl = `${deps.upstreamBaseUrl}${path}${search}`;
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
+ let upstream: Response;
+ try {
+ upstream = await deps.fetchImpl(upstreamUrl, {
+ method: request.method,
+ headers: buildUpstreamHeaders(request, resolved.bearer),
+ body: hasBody ? request.body : undefined,
+ signal: request.signal,
+ redirect: "manual",
+ ...(hasBody ? { duplex: "half" } : {}),
+ } as RequestInit);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : "upstream request failed";
+ recordCall(deps, {
+ method: request.method,
+ path,
+ status: HTTP_STATUS.BAD_GATEWAY,
+ startedAt,
+ principal,
+ connectionId: resolved.connectionId,
+ error: message,
+ });
+ return connectError(HTTP_STATUS.BAD_GATEWAY, "unavailable", message);
+ }
+
+ if (upstream.status === HTTP_STATUS.UNAUTHORIZED && resolved.apiKey) {
+ deps.invalidateBearer(resolved.apiKey);
+ }
+
+ const logInput: CallLogInput = {
+ method: request.method,
+ path,
+ status: upstream.status,
+ startedAt,
+ principal,
+ connectionId: resolved.connectionId,
+ };
+ const headers = buildDownstreamHeaders(upstream);
+ if (!upstream.body) {
+ recordCall(deps, logInput);
+ return new Response(null, { status: upstream.status, headers });
+ }
+ const body = streamWithCompletionLog(upstream.body, (error) =>
+ recordCall(deps, { ...logInput, error: error ?? null })
+ );
+ return new Response(body, { status: upstream.status, headers });
+}
+
+export async function handleCursorCliProxy(
+ request: Request,
+ segments: readonly string[],
+ overrides: Partial = {}
+): Promise {
+ const deps: CursorCliProxyDeps = { ...defaultDeps, ...overrides };
+ const startedAt = deps.now();
+ const path = normalizeCursorCliPath(segments);
+ if (path === CURSOR_API_KEY_EXCHANGE_PATH) {
+ return handleExchange(request, startedAt, deps);
+ }
+ return handleForward(request, path, startedAt, deps);
+}
diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts
index 7fb6357db12c..c8312ab97437 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,
@@ -54,6 +54,7 @@ import { handleGeminiWebImageGeneration } from "./imageGeneration/providers/gemi
import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts";
import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmind.ts";
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
+import { handleCursorAgentImageGeneration } from "./imageGeneration/providers/cursorAgentImage.ts";
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
import { handleAdobeFireflyImageGeneration } from "./imageGeneration/providers/adobeFirefly.ts";
import { handleAlibabaImageGeneration } from "./imageGeneration/providers/alibabaImage.ts";
@@ -184,6 +185,29 @@ function sanitizeImageProviderError(errorText: string): unknown {
return sanitizeErrorMessage(errorText);
}
+// #8307 — some ChatGPT accounts can run Codex but lack entitlement for the specific
+// requested image model. Upstream signals this as a 400 with an exact, stable message
+// (not a generic "invalid request"). Classify it so the caller can mark the failure
+// `retryable: true`, which routes it through the same sibling-account fallback that
+// already handles 401s (executeImageWithCredentialFallback, src/sse/services/imageCredentialRetry.ts).
+function isCodexChatGptModelAccessError(status: number, errorText: string, model: string): boolean {
+ if (status !== 400) return false;
+ const parsed = parseJsonOrNull(errorText);
+ let detail: string | null = null;
+ if (typeof parsed === "string") {
+ detail = parsed;
+ } else if (parsed && typeof parsed === "object") {
+ const obj = parsed as Record;
+ if (typeof obj.detail === "string") detail = obj.detail;
+ else if (typeof obj.message === "string") detail = obj.message;
+ else if (obj.error && typeof obj.error === "object") {
+ const nested = (obj.error as Record).message;
+ if (typeof nested === "string") detail = nested;
+ }
+ }
+ return detail === `The '${model}' model is not supported when using Codex with a ChatGPT account.`;
+}
+
const BFL_MODEL_ENDPOINTS = {
"flux-2-max": "/v1/flux-2-max",
"flux-2-pro": "/v1/flux-2-pro",
@@ -277,6 +301,10 @@ const FAL_PRESET_SIZES = {
* @param {object} options.credentials - Provider credentials { apiKey, accessToken }
* @param {object} options.log - Logger
* @param {string} [options.resolvedProvider] - Pre-resolved provider ID (from route layer custom model resolution)
+ * @param {string|null} [options.peerLocality] - Trusted "loopback"|"lan"|"remote" verdict
+ * forwarded from `AUTHZ_HEADER_PEER_LOCALITY` (src/server/authz/headers.ts). Only consumed by
+ * spawn-capable providers (e.g. cursor-agent-image) to enforce Hard Rules #15/#17 without
+ * loopback-gating the whole route for every non-spawning image provider.
*/
export async function handleImageGeneration({
body,
@@ -285,6 +313,7 @@ export async function handleImageGeneration({
resolvedProvider = null,
signal = null,
clientHeaders = null,
+ peerLocality = null,
}) {
let provider, model;
@@ -495,6 +524,18 @@ export async function handleImageGeneration({
});
}
+ if (providerConfig.format === "cursor-agent-image") {
+ return handleCursorAgentImageGeneration({
+ model,
+ provider,
+ providerConfig,
+ body,
+ credentials,
+ log,
+ peerLocality,
+ });
+ }
+
if (providerConfig.format === "designer-web") {
return handleDesignerWebImageGeneration({
model,
@@ -590,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,
@@ -1305,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,
@@ -2532,6 +2674,7 @@ async function handleCodexImageGeneration({
const safeErrorLog =
typeof safeError === "string" ? safeError : JSON.stringify(safeError ?? {});
if (log) log.error("IMAGE", `${provider} error ${response.status}: ${safeErrorLog}`);
+ const retryable = isCodexChatGptModelAccessError(response.status, errorText, model);
return {
ok: false as const,
error: {
@@ -2542,6 +2685,7 @@ async function handleCodexImageGeneration({
error: safeError,
requestBody: requestBodyForLog,
path: logPath,
+ ...(retryable ? { retryable: true } : {}),
},
};
}
diff --git a/open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts b/open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts
new file mode 100644
index 000000000000..a05b7ef85439
--- /dev/null
+++ b/open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts
@@ -0,0 +1,488 @@
+/**
+ * Cursor Agent image generation — OpenAI `/v1/images/generations` backed by the
+ * Cursor Agent CLI's native `generateImage` tool (real diffusion, not SVG).
+ *
+ * Why CLI (not AgentService/Run): OmniRoute's Cursor chat executor talks to
+ * `agent.v1.AgentService/Run` over protobuf and **rejects** built-in tools
+ * (shell/write/…). Image generation is a Cursor-native client tool that the
+ * `agent` binary executes locally against the seat. Spawning the CLI with a
+ * locked prompt + per-request workspace mirrors the proven seat bridge shape
+ * and reuses the same `provider_connections` row as chat (`provider: "cursor"`).
+ *
+ * Auth: `credentials.accessToken` / `apiKey` from the Cursor OAuth (or API-key)
+ * connection. Tokens matching `crsr_…` are exported as `CURSOR_API_KEY`; other
+ * session JWTs as `CURSOR_AUTH_TOKEN`. The `account::token` composite used by
+ * the chat executor is normalized the same way (`split("::")[1]`).
+ *
+ * Binary: `CURSOR_AGENT_BIN` → `providerSpecificData.agentBin` → PATH / default
+ * shim under `~/.local/bin/agent`. Missing binary → HTTP 501 with install hint.
+ */
+
+import { spawn } from "node:child_process";
+import { existsSync } from "node:fs";
+import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
+import { homedir, tmpdir } from "node:os";
+import { join } from "node:path";
+import { sanitizeErrorMessage } from "../../../utils/error.ts";
+import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
+import { IMAGE_PROVIDERS } from "../../../config/imageRegistry.ts";
+
+export const CURSOR_AGENT_IMAGE_FORMAT = "cursor-agent-image";
+
+const DEFAULT_TIMEOUT_MS = 210_000;
+const DEFAULT_MAX_CONCURRENT = 2;
+const DEFAULT_MODEL = "auto";
+const MAX_N = 4;
+
+// Upper bound on a caller-supplied `timeout_ms`. The Cursor seat is shared and
+// CURSOR_IMG_MAX_CONCURRENT defaults to only 2 slots, so a huge per-request
+// timeout must not hog a slot and starve every other caller.
+const MAX_TIMEOUT_MS = 300_000;
+
+// Models the Agent CLI `--model` argv may receive — kept in sync with the
+// registry entry (auto | composer-2 | composer-2.5). The request `model` is
+// untrusted input forwarded straight into a spawned CLI, so we mirror the
+// auggie executor: anything outside this set (unknown model, or a flag-shaped
+// value like "--foo" / "-x") is clamped to DEFAULT_MODEL and never reaches argv.
+const CURSOR_IMAGE_MODEL_ALLOWLIST: ReadonlySet = new Set(
+ (IMAGE_PROVIDERS.cursor?.models ?? []).map((m) => m.id)
+);
+
+/** Clamp a model candidate to the allowlist; unknown/flag-shaped → "auto". */
+export function resolveCursorImageModel(candidate: unknown): string {
+ const requested = typeof candidate === "string" ? candidate.trim() : "";
+ return CURSOR_IMAGE_MODEL_ALLOWLIST.has(requested) ? requested : DEFAULT_MODEL;
+}
+
+const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
+const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
+
+/**
+ * Localities allowed to trigger the `agent` binary spawn below (Hard Rules
+ * #15 + #17). `/v1/images/generations` is a normal remote-reachable inference
+ * route shared by ~40 image providers that only proxy HTTP — the ONLY branch
+ * here that spawns a child process is this one, so the whole route cannot be
+ * classified in `LOCAL_ONLY_API_PREFIXES` (routeGuard.ts) without blocking
+ * every other, non-spawning image provider for remote callers. Instead this
+ * handler enforces its OWN loopback/LAN gate using the trusted locality
+ * verdict the authz pipeline already stamps on every request
+ * (`AUTHZ_HEADER_PEER_LOCALITY`, src/server/authz/headers.ts, computed from
+ * the real TCP peer IP — never the spoofable Host header). Mirrors the
+ * loopback-or-private-LAN policy `managementPolicy` applies to every other
+ * LOCAL_ONLY route (src/server/authz/policies/management.ts).
+ */
+const SPAWN_ALLOWED_LOCALITIES = new Set(["loopback", "lan"]);
+
+/** Locked instruction — ingress callers can only trigger image gen, never a shell. */
+export function buildCursorAgentImagePrompt(userPrompt: string, outPath: string, size?: unknown): string {
+ const sizeHint =
+ typeof size === "string" && size.trim() ? ` Target size/aspect: ${size.trim()}.` : "";
+ return [
+ "You have a native image-generation tool. Use it to generate ONE image.",
+ "Do NOT write code, do NOT hand-author SVG, do NOT install packages — use your built-in image generation.",
+ `Image to generate: ${userPrompt}.${sizeHint}`,
+ `Save the resulting image to exactly this path: ${outPath}.`,
+ "When the file exists at that exact path, reply with only the word DONE.",
+ ].join(" ");
+}
+
+/** Strip OmniRoute `account::token` composites the same way CursorExecutor does. */
+export function normalizeCursorSeatToken(raw: string): string {
+ const trimmed = raw.trim();
+ if (!trimmed) return trimmed;
+ return trimmed.includes("::") ? trimmed.split("::").slice(1).join("::").trim() || trimmed : trimmed;
+}
+
+/**
+ * Map a Cursor connection token into the env vars the Agent CLI reads.
+ * Prefer API keys (`crsr_…`) as `CURSOR_API_KEY`; otherwise session JWT → `CURSOR_AUTH_TOKEN`.
+ */
+export function buildCursorAgentAuthEnv(token: string): Record {
+ const clean = normalizeCursorSeatToken(token);
+ if (clean.startsWith("crsr_")) {
+ return { CURSOR_API_KEY: clean };
+ }
+ return { CURSOR_AUTH_TOKEN: clean };
+}
+
+export function resolveCursorAgentBin(override?: string | null): string | null {
+ // Explicit connection override wins even when the path is missing — the handler
+ // returns 501 so operators see a clear misconfiguration instead of a silent fallback.
+ if (typeof override === "string" && override.trim()) {
+ return override.trim();
+ }
+ const envBin = process.env.CURSOR_AGENT_BIN?.trim();
+ if (envBin) return envBin;
+
+ const defaultShim = join(homedir(), ".local", "bin", "agent");
+ if (existsSync(defaultShim)) return defaultShim;
+
+ // Last resort: bare `agent` on PATH (spawn fails with ENOENT → 501).
+ return "agent";
+}
+
+export function isRasterImageBuffer(buf: Buffer): boolean {
+ if (buf.length >= 8 && buf.subarray(0, 8).equals(PNG_MAGIC)) return true;
+ if (buf.length >= 3 && buf.subarray(0, 3).equals(JPEG_MAGIC)) return true;
+ return false;
+}
+
+export async function findCursorAgentImageOutput(
+ workspace: string,
+ preferredPath: string
+): Promise {
+ if (existsSync(preferredPath)) return preferredPath;
+ try {
+ const entries = await readdir(workspace);
+ const match = entries.find((name) => /\.(png|jpe?g|webp)$/i.test(name));
+ return match ? join(workspace, match) : null;
+ } catch {
+ return null;
+ }
+}
+
+function normalizePositiveInt(value: unknown, fallback: number, max?: number): number {
+ const n = Number(value);
+ if (!Number.isFinite(n) || n <= 0) return fallback;
+ const i = Math.floor(n);
+ return typeof max === "number" ? Math.min(i, max) : i;
+}
+
+/**
+ * Effective per-image wall clock: a caller-supplied `timeout_ms` clamped to
+ * MAX_TIMEOUT_MS. When the request omits it, fall back to the operator default
+ * (CURSOR_IMG_TIMEOUT_MS) / DEFAULT_TIMEOUT_MS uncapped — operator config is
+ * trusted; only the untrusted request value is clamped.
+ */
+export function resolveCursorImageTimeoutMs(rawTimeout: unknown): number {
+ return normalizePositiveInt(
+ rawTimeout,
+ normalizePositiveInt(process.env.CURSOR_IMG_TIMEOUT_MS, DEFAULT_TIMEOUT_MS),
+ MAX_TIMEOUT_MS
+ );
+}
+
+type CursorAgentImageCredentials = {
+ apiKey?: string;
+ accessToken?: string;
+ providerSpecificData?: Record | null;
+};
+
+function extractSeatToken(credentials: CursorAgentImageCredentials): string {
+ const raw = credentials?.accessToken || credentials?.apiKey || "";
+ return typeof raw === "string" ? raw.trim() : "";
+}
+
+function extractAgentBinOverride(credentials: CursorAgentImageCredentials): string | null {
+ const psd = credentials?.providerSpecificData;
+ if (!psd || typeof psd !== "object" || Array.isArray(psd)) return null;
+ const bin = psd.agentBin;
+ return typeof bin === "string" && bin.trim() ? bin.trim() : null;
+}
+
+function extractAgentModel(credentials: CursorAgentImageCredentials, requestModel: string): string {
+ const psd = credentials?.providerSpecificData;
+ if (psd && typeof psd === "object" && !Array.isArray(psd)) {
+ const fromPsd = psd.imageModel;
+ if (typeof fromPsd === "string" && fromPsd.trim()) return fromPsd.trim();
+ }
+ if (process.env.CURSOR_IMG_MODEL?.trim()) return process.env.CURSOR_IMG_MODEL.trim();
+ // The request's `model=cursor/<…>` field is untrusted and flows into the CLI
+ // `--model` argv — clamp it to the registry allowlist (unknown/flag-shaped →
+ // "auto"). The operator overrides above (connection psd / CURSOR_IMG_MODEL)
+ // are trusted deployment config and pass through unchanged.
+ return resolveCursorImageModel(
+ requestModel && requestModel !== "cursor" ? requestModel : DEFAULT_MODEL
+ );
+}
+
+// ─── process-wide concurrency gate (one shared Cursor seat) ─────────────────
+
+type Waiter = () => void;
+let activeGenerations = 0;
+const waitQueue: Waiter[] = [];
+
+export function __resetCursorAgentImageConcurrencyForTests(): void {
+ activeGenerations = 0;
+ waitQueue.length = 0;
+}
+
+function maxConcurrent(): number {
+ return normalizePositiveInt(process.env.CURSOR_IMG_MAX_CONCURRENT, DEFAULT_MAX_CONCURRENT);
+}
+
+async function acquireSlot(): Promise {
+ if (activeGenerations < maxConcurrent()) {
+ activeGenerations += 1;
+ return;
+ }
+ await new Promise((resolve) => {
+ waitQueue.push(() => {
+ activeGenerations += 1;
+ resolve();
+ });
+ });
+}
+
+function releaseSlot(): void {
+ activeGenerations = Math.max(0, activeGenerations - 1);
+ const next = waitQueue.shift();
+ if (next) next();
+}
+
+export type RunCursorAgentImageOptions = {
+ agentBin: string;
+ workspace: string;
+ prompt: string;
+ model: string;
+ authEnv: Record;
+ timeoutMs: number;
+ spawnImpl?: typeof spawn;
+};
+
+/** Spawn `agent -p --force …` and resolve when it exits 0 (or reject on timeout/error). */
+export function runCursorAgentImageProcess(opts: RunCursorAgentImageOptions): Promise<{
+ stdout: string;
+ stderr: string;
+}> {
+ const spawnImpl = opts.spawnImpl ?? spawn;
+ const args = [
+ "-p",
+ "--force",
+ "--model",
+ opts.model,
+ "--workspace",
+ opts.workspace,
+ "--output-format",
+ "text",
+ opts.prompt,
+ ];
+
+ return new Promise((resolve, reject) => {
+ const child = spawnImpl(opts.agentBin, args, {
+ cwd: opts.workspace,
+ env: {
+ ...process.env,
+ ...opts.authEnv,
+ HOME: process.env.HOME || homedir(),
+ },
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ let stdout = "";
+ let stderr = "";
+ const timer = setTimeout(() => {
+ child.kill("SIGKILL");
+ reject(new Error(`Cursor Agent image generation timed out after ${opts.timeoutMs}ms`));
+ }, opts.timeoutMs);
+
+ child.stdout?.on("data", (chunk: Buffer | string) => {
+ stdout += String(chunk);
+ });
+ child.stderr?.on("data", (chunk: Buffer | string) => {
+ stderr += String(chunk);
+ });
+ child.on("error", (err) => {
+ clearTimeout(timer);
+ reject(err);
+ });
+ child.on("close", (code) => {
+ clearTimeout(timer);
+ if (code === 0) {
+ resolve({ stdout, stderr });
+ return;
+ }
+ reject(
+ new Error(
+ `Cursor Agent exited ${code}: ${(stderr || stdout).trim().slice(0, 400) || "no output"}`
+ )
+ );
+ });
+ });
+}
+
+async function generateOneImage(params: {
+ userPrompt: string;
+ size: unknown;
+ agentBin: string;
+ model: string;
+ authEnv: Record;
+ timeoutMs: number;
+ spawnImpl?: typeof spawn;
+}): Promise {
+ const workspace = await mkdtemp(join(tmpdir(), "omni-cursor-img-"));
+ const outPath = join(workspace, "out.png");
+ const prompt = buildCursorAgentImagePrompt(params.userPrompt, outPath, params.size);
+
+ try {
+ await runCursorAgentImageProcess({
+ agentBin: params.agentBin,
+ workspace,
+ prompt,
+ model: params.model,
+ authEnv: params.authEnv,
+ timeoutMs: params.timeoutMs,
+ spawnImpl: params.spawnImpl,
+ });
+
+ const found = await findCursorAgentImageOutput(workspace, outPath);
+ if (!found) {
+ throw new Error("Cursor Agent produced no image file in the workspace");
+ }
+ const buf = await readFile(found);
+ if (!isRasterImageBuffer(buf)) {
+ throw new Error("Cursor Agent output is not a PNG/JPEG raster");
+ }
+ return buf;
+ } finally {
+ await rm(workspace, { recursive: true, force: true }).catch(() => {});
+ }
+}
+
+export async function handleCursorAgentImageGeneration({
+ model,
+ provider,
+ providerConfig: _providerConfig,
+ body,
+ credentials,
+ log,
+ spawnImpl,
+ peerLocality,
+}: {
+ model: string;
+ provider: string;
+ providerConfig: { baseUrl?: string };
+ body: {
+ prompt?: unknown;
+ size?: unknown;
+ n?: unknown;
+ timeout_ms?: unknown;
+ };
+ credentials: CursorAgentImageCredentials;
+ log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
+ /** Test seam — defaults to node:child_process.spawn */
+ spawnImpl?: typeof spawn;
+ /**
+ * Trusted locality verdict ("loopback" | "lan" | "remote") forwarded by the
+ * route layer from `AUTHZ_HEADER_PEER_LOCALITY` (stamped by the authz
+ * pipeline from the real TCP peer, never the spoofable Host header). Absent
+ * or unrecognized → fail closed (treated as "remote").
+ */
+ peerLocality?: string | null;
+}) {
+ const startTime = Date.now();
+
+ // Hard Rules #15 + #17: reject before doing ANY other work — credential
+ // lookup, prompt validation, and the `agent` binary spawn itself must never
+ // run for a non-loopback/non-LAN caller. A leaked API key tunneled from the
+ // public internet must not be able to trigger a child-process spawn on the
+ // OmniRoute host.
+ if (!peerLocality || !SPAWN_ALLOWED_LOCALITIES.has(peerLocality)) {
+ return saveImageErrorResult({
+ provider,
+ model,
+ status: 403,
+ startTime,
+ error:
+ "Cursor Agent image generation spawns a local process and is only available from localhost or the private LAN OmniRoute runs on.",
+ });
+ }
+
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
+ if (!prompt) {
+ return saveImageErrorResult({
+ provider,
+ model,
+ status: 400,
+ startTime,
+ error: "Prompt is required for Cursor Agent image generation",
+ });
+ }
+
+ const token = extractSeatToken(credentials);
+ if (!token) {
+ return saveImageErrorResult({
+ provider,
+ model,
+ status: 401,
+ startTime,
+ error: "Cursor credentials missing accessToken — reconnect the Cursor provider",
+ });
+ }
+
+ const agentBin = resolveCursorAgentBin(extractAgentBinOverride(credentials));
+ if (!agentBin || (agentBin !== "agent" && !existsSync(agentBin))) {
+ // Bare "agent" may still resolve via PATH; only hard-fail when an explicit path is missing.
+ if (agentBin !== "agent") {
+ return saveImageErrorResult({
+ provider,
+ model,
+ status: 501,
+ startTime,
+ error:
+ "Cursor Agent CLI not found. Install the Cursor `agent` binary and set CURSOR_AGENT_BIN, or set providerSpecificData.agentBin on the Cursor connection.",
+ });
+ }
+ }
+
+ const timeoutMs = resolveCursorImageTimeoutMs(body.timeout_ms);
+ const count = normalizePositiveInt(body.n, 1, MAX_N);
+ const agentModel = extractAgentModel(credentials, model);
+ const authEnv = buildCursorAgentAuthEnv(token);
+
+ if (log?.info) {
+ log.info(
+ "IMAGE",
+ `${provider}/${model} (cursor-agent-image) | n=${count} model=${agentModel} bin=${agentBin}`
+ );
+ }
+
+ const images: Array<{ b64_json: string; revised_prompt: string }> = [];
+
+ try {
+ for (let i = 0; i < count; i++) {
+ await acquireSlot();
+ try {
+ const buf = await generateOneImage({
+ userPrompt: prompt,
+ size: body.size,
+ agentBin: agentBin || "agent",
+ model: agentModel,
+ authEnv,
+ timeoutMs,
+ spawnImpl,
+ });
+ images.push({ b64_json: buf.toString("base64"), revised_prompt: prompt });
+ } finally {
+ releaseSlot();
+ }
+ }
+
+ return saveImageSuccessResult({
+ provider,
+ model,
+ startTime,
+ images,
+ });
+ } catch (err) {
+ const errorText = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
+ if (log?.error) {
+ log.error("IMAGE", `${provider} cursor-agent-image error: ${errorText}`);
+ }
+ // ENOENT from spawn → treat as missing CLI
+ const status =
+ err && typeof err === "object" && "code" in err && (err as { code?: string }).code === "ENOENT"
+ ? 501
+ : 502;
+ return saveImageErrorResult({
+ provider,
+ model,
+ status,
+ startTime,
+ error:
+ status === 501
+ ? "Cursor Agent CLI not found on PATH. Set CURSOR_AGENT_BIN to the `agent` binary."
+ : errorText,
+ });
+ }
+}
diff --git a/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 2f3320cf0688..31ea68632777 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/handlers/mediaGeneration/minimaxMusic.ts b/open-sse/handlers/mediaGeneration/minimaxMusic.ts
new file mode 100644
index 000000000000..34866760585a
--- /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 92ee333c2efc..89052b3765fc 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/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts
index 06d3202f5f75..a2210681d7f1 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 407393966dc3..01bdd4c14a74 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/handlers/search.ts b/open-sse/handlers/search.ts
index 42974dc8fa9a..ca6221538a20 100644
--- a/open-sse/handlers/search.ts
+++ b/open-sse/handlers/search.ts
@@ -31,6 +31,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import { z } from "zod";
import { sanitizeErrorMessage } from "../utils/error.ts";
import { resolveSearchProxy, executeProviderFetch } from "./search/searchProxy.ts";
+import { formatSearchProviderFailure } from "./search/providerFailure.ts";
export interface SearchResult {
title: string;
@@ -1177,11 +1178,7 @@ async function tryZaiMCPProvider(
/* non-critical — logging must not block search response */
});
- return {
- success: false,
- status: isTimeout ? 504 : 502,
- error: `Search provider ${isTimeout ? "timeout" : "error"}: ${sanitizeErrorMessage(err.message)}`,
- };
+ return formatSearchProviderFailure(config.id, err, isTimeout);
}
}
diff --git a/open-sse/handlers/search/providerFailure.ts b/open-sse/handlers/search/providerFailure.ts
new file mode 100644
index 000000000000..e021c2fa80b7
--- /dev/null
+++ b/open-sse/handlers/search/providerFailure.ts
@@ -0,0 +1,26 @@
+import { sanitizeErrorMessage } from "../../utils/error.ts";
+
+export interface SearchProviderFailure {
+ success: false;
+ status: number;
+ error: string;
+}
+
+/** Named 502/504 for /v1/search — provider id + sanitized cause, no hostnames/URLs. */
+export function formatSearchProviderFailure(
+ providerId: string,
+ err: unknown,
+ isTimeout: boolean
+): SearchProviderFailure {
+ const rec = err && typeof err === "object" ? (err as Record) : {};
+ const cause = rec.cause && typeof rec.cause === "object" ? (rec.cause as Record) : {};
+ const code =
+ typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{1,39}$/.test(cause.code) ? cause.code : "";
+ const msg =
+ sanitizeErrorMessage(typeof rec.message === "string" ? rec.message : "fetch failed") || "fetch failed";
+ return {
+ success: false,
+ status: isTimeout ? 504 : 502,
+ error: `Search provider ${providerId} ${isTimeout ? "timeout" : "error"}: ${code ? `${msg} (cause: ${code})` : msg}`,
+ };
+}
diff --git a/open-sse/handlers/search/searchProxy.ts b/open-sse/handlers/search/searchProxy.ts
index f134b4a3bdc2..75faea4db7ec 100644
--- a/open-sse/handlers/search/searchProxy.ts
+++ b/open-sse/handlers/search/searchProxy.ts
@@ -10,6 +10,7 @@
import { saveCallLog } from "@/lib/usageDb";
import { sanitizeErrorMessage } from "../../utils/error.ts";
+import { formatSearchProviderFailure } from "./providerFailure.ts";
import type { SearchProviderConfig } from "../../config/searchRegistry.ts";
import type { SearchResult } from "../search.ts";
@@ -231,15 +232,12 @@ export async function executeProviderFetch(p: ExecuteProviderFetchParams): Promi
clearTimeout(timer);
const error = err instanceof Error ? err : new Error(String(err));
const isTimeout = error.name === "AbortError";
+ const safeMsg = sanitizeErrorMessage(error.message) || "fetch failed";
if (log) {
- log.error("SEARCH", `${config.id} ${isTimeout ? "timeout" : "fetch error"}: ${error.message}`);
+ log.error("SEARCH", `${config.id} ${isTimeout ? "timeout" : "fetch error"}: ${safeMsg}`);
}
- logCall({ status: isTimeout ? 504 : 502, duration: Date.now() - startTime, error: error.message });
+ logCall({ status: isTimeout ? 504 : 502, duration: Date.now() - startTime, error: safeMsg });
await emitEvent(isTimeout ? "timeout" : "error");
- return {
- success: false,
- status: isTimeout ? 504 : 502,
- error: `Search provider ${isTimeout ? "timeout" : "error"}: ${sanitizeErrorMessage(error.message)}`,
- };
+ return formatSearchProviderFailure(config.id, error, isTimeout);
}
}
diff --git a/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts b/open-sse/mcp-server/__tests__/glmCodingProviderConfig.test.ts
index 52ce967e1f0e..8b7f3cee3240 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
new file mode 100644
index 000000000000..e3cea5bcce83
--- /dev/null
+++ b/open-sse/mcp-server/fetchTimeout.ts
@@ -0,0 +1,72 @@
+/**
+ * #9717 — timeout policy for the MCP server's internal server→server fetches.
+ *
+ * `omniRouteFetch` serves two call shapes with very different latency budgets:
+ * fast local management reads (health, resilience, combos, quota, usage) and
+ * calls that wait on an upstream provider. A single 10s default aborted
+ * `omniroute_route_request` while the upstream request was still in flight,
+ * even though `omniroute_web_search` / `omniroute_web_fetch` already carried
+ * their own explicit 60s signal in the same file for exactly that reason.
+ *
+ * Kept as a pure, dependency-free module so the policy is unit-testable without
+ * starting the MCP server, mirroring how `tools/poolTools.ts` keeps handlers
+ * separate from server wiring.
+ */
+
+/** Local management reads — a stalled one should fail fast, not hold a tool call open. */
+export const MCP_FETCH_TIMEOUT_MS = 10_000;
+
+/**
+ * Calls that wait on an upstream provider. 60s is not a new number: it is the
+ * value `web_search`/`web_fetch` already used, now shared with model routing
+ * instead of each call site picking its own literal.
+ */
+export const MCP_UPSTREAM_FETCH_TIMEOUT_MS = 60_000;
+
+export const MCP_FETCH_TIMEOUT_ENV = "OMNIROUTE_MCP_FETCH_TIMEOUT_MS";
+export const MCP_UPSTREAM_FETCH_TIMEOUT_ENV = "OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS";
+
+export type McpFetchTimeoutKind = "management" | "upstream";
+
+function readPositiveIntEnv(raw: string | undefined): number | null {
+ if (typeof raw !== "string" || raw.trim() === "") return null;
+ const parsed = Number(raw);
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
+}
+
+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
+ * disabling the timeout — a bad env value must not turn a bounded wait into an
+ * unbounded one.
+ */
+export function resolveMcpFetchTimeoutMs(
+ kind: McpFetchTimeoutKind,
+ env: Record = process.env
+): number {
+ const 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. */
+export function mcpFetchTimeoutSignal(
+ kind: McpFetchTimeoutKind,
+ env?: Record
+): AbortSignal {
+ return AbortSignal.timeout(resolveMcpFetchTimeoutMs(kind, env));
+}
diff --git a/open-sse/mcp-server/httpTransport.ts b/open-sse/mcp-server/httpTransport.ts
index ab742858c6a7..d8826c738ca3 100644
--- a/open-sse/mcp-server/httpTransport.ts
+++ b/open-sse/mcp-server/httpTransport.ts
@@ -284,12 +284,30 @@ export async function handleMcpStreamableHTTP(request: Request): Promise {
+ if (request.method === "POST") {
+ try {
+ const body = await request.clone().json();
+ const isInitialize = Array.isArray(body)
+ ? body.some((req: RpcRequest) => req?.method === "initialize")
+ : (body as RpcRequest)?.method === "initialize";
+
+ if (isInitialize) {
+ console.log("[MCP] New client initialize detected, resetting SSE singleton...");
+ closeSseTransport();
+ }
+ } catch (err) {}
+ }
const { transport } = ensureSseServer();
try {
diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts
index f5634816e105..b2a866e99763 100644
--- a/open-sse/mcp-server/server.ts
+++ b/open-sse/mcp-server/server.ts
@@ -92,6 +92,7 @@ import { getDbInstance } from "../../src/lib/db/core.ts";
import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts";
import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
+import { mcpFetchTimeoutSignal } from "./fetchTimeout.ts";
import { getMcpModelsCatalog } from "./catalog.ts";
import { registerRadarCatalogTool } from "./radarCatalog.ts";
import type { TextToolResult } from "./toolResult.ts";
@@ -213,7 +214,7 @@ export async function omniRouteFetch(path: string, options: RequestInit = {}): P
...getInternalServiceAuthHeaders(),
};
- const signal = options.signal || AbortSignal.timeout(10000);
+ const signal = options.signal || mcpFetchTimeoutSignal("management");
const response = await fetch(url, { ...options, headers, signal });
if (!response.ok) {
@@ -518,6 +519,10 @@ async function handleRouteRequest(args: {
const raw = (await omniRouteFetch("/v1/chat/completions", {
method: "POST",
body: JSON.stringify(body),
+ // #9717: this hop waits on an upstream provider (and on auto-combo
+ // candidate probing before one is even chosen), so it must not inherit
+ // the management-read budget.
+ signal: mcpFetchTimeoutSignal("upstream"),
})) as JsonRecord;
const choices = toArray(raw.choices);
const firstChoice = toRecord(choices[0]);
@@ -648,7 +653,7 @@ async function handleWebSearch(args: {
const result = await omniRouteFetch("/v1/search", {
method: "POST",
body: JSON.stringify(body),
- signal: AbortSignal.timeout(60000),
+ signal: mcpFetchTimeoutSignal("upstream"),
});
await logToolCall("omniroute_web_search", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
@@ -681,7 +686,7 @@ async function handleWebFetch(args: {
const result = await omniRouteFetch("/v1/web/fetch", {
method: "POST",
body: JSON.stringify(body),
- signal: AbortSignal.timeout(60000),
+ signal: mcpFetchTimeoutSignal("upstream"),
});
await logToolCall("omniroute_web_fetch", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
diff --git a/open-sse/mcp-server/tools/memoryTools.ts b/open-sse/mcp-server/tools/memoryTools.ts
index 908970f2e1d5..16c835fd8e51 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